feat: unify all colors

This commit is contained in:
Aarnav Tale 2025-02-04 17:21:03 -05:00
parent d1f6c450c0
commit 287ac2dff0
No known key found for this signature in database
30 changed files with 233 additions and 284 deletions

View File

@ -54,7 +54,7 @@ export default function Attribute({
}, 1000); }, 1000);
}} }}
> >
{value} <p className="truncate">{value}</p>
<Check className="h-4.5 w-4.5 p-1 hidden data-[copied]:block" /> <Check className="h-4.5 w-4.5 p-1 hidden data-[copied]:block" />
<Copy className="h-4.5 w-4.5 p-1 block data-[copied]:hidden" /> <Copy className="h-4.5 w-4.5 p-1 block data-[copied]:hidden" />
</button> </button>

View File

@ -1,14 +1,19 @@
import { LinkExternalIcon } from '@primer/octicons-react'; import { ExternalLink } from 'lucide-react';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
interface Props { export interface LinkProps {
to: string; to: string;
name: string; name: string;
children: string; children: string;
className?: string; className?: string;
} }
export default function Link({ to, name: alt, children, className }: Props) { export default function Link({
to,
name: alt,
children,
className,
}: LinkProps) {
return ( return (
<a <a
href={to} href={to}
@ -16,14 +21,14 @@ export default function Link({ to, name: alt, children, className }: Props) {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className={cn( className={cn(
'inline-flex items-center gap-x-1', 'inline-flex items-center gap-x-0.5',
'text-blue-500 hover:text-blue-700', 'text-blue-500 hover:text-blue-700',
'dark:text-blue-400 dark:hover:text-blue-300', 'dark:text-blue-400 dark:hover:text-blue-300',
className, className,
)} )}
> >
{children} {children}
<LinkExternalIcon className="h-3 w-3" /> <ExternalLink className="w-3.5" />
</a> </a>
); );
} }

View File

@ -97,7 +97,10 @@ function MenuSection<T>({ section, state }: MenuSectionProps<T>) {
{section.key !== state.collection.getFirstKey() ? ( {section.key !== state.collection.getFirstKey() ? (
<li <li
{...separatorProps} {...separatorProps}
className="border-t border-gray-300 mx-2 mt-1 mb-1" className={cn(
'mx-2 mt-1 mb-1 border-t',
'border-headplane-200 dark:border-headplane-800',
)}
/> />
) : undefined} ) : undefined}
<li {...itemProps}> <li {...itemProps}>

View File

@ -1,23 +1,16 @@
import { InfoIcon } from '@primer/octicons-react'; import { CircleSlash2 } from 'lucide-react';
import type { ReactNode } from 'react'; import React from 'react';
import cn from '~/utils/cn'; import Card from '~/components/Card';
interface Props { export interface NoticeProps {
className?: string; children: React.ReactNode;
children: ReactNode;
} }
export default function Notice({ children, className }: Props) { export default function Notice({ children }: NoticeProps) {
return ( return (
<div <Card className="flex w-full max-w-full gap-4 font-semibold">
className={cn( <CircleSlash2 />
'p-4 rounded-md w-full flex items-center gap-3',
'bg-ui-200 dark:bg-ui-800',
className,
)}
>
<InfoIcon className="h-6 w-6 text-ui-700 dark:text-ui-200" />
{children} {children}
</div> </Card>
); );
} }

View File

@ -14,7 +14,9 @@ import { Item, ListState, Node, useComboBoxState } from 'react-stately';
import Popover from '~/components/Popover'; import Popover from '~/components/Popover';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
export interface SelectProps extends AriaComboBoxProps<object> {} export interface SelectProps extends AriaComboBoxProps<object> {
className?: string;
}
function Select(props: SelectProps) { function Select(props: SelectProps) {
const { contains } = useFilter({ sensitivity: 'base' }); const { contains } = useFilter({ sensitivity: 'base' });
@ -45,7 +47,7 @@ function Select(props: SelectProps) {
const { buttonProps } = useButton(triggerProps, buttonRef); const { buttonProps } = useButton(triggerProps, buttonRef);
return ( return (
<div className="flex flex-col"> <div className={cn('flex flex-col', props.className)}>
<label <label
{...labelProps} {...labelProps}
htmlFor={id} htmlFor={id}

View File

@ -6,14 +6,13 @@ interface Props {
export default function Spinner({ className }: Props) { export default function Spinner({ className }: Props) {
return ( return (
<div className={clsx('mr-1.5 inline-block align-middle mb-0.5', className)}> <div className={clsx('inline-block align-middle mb-0.5', className)}>
<div <div
className={clsx( className={clsx(
'animate-spin rounded-full w-full h-full', 'animate-spin rounded-full w-full h-full',
'border-2 border-current border-t-transparent', 'border-2 border-current border-t-transparent',
className, className,
)} )}
role="status"
> >
<span className="sr-only">Loading...</span> <span className="sr-only">Loading...</span>
</div> </div>

View File

@ -1,23 +1,26 @@
import clsx from 'clsx'; import cn from '~/utils/cn';
import type { HTMLProps } from 'react';
type Props = HTMLProps<SVGElement> & { export interface StatusCircleProps {
readonly isOnline: boolean; isOnline: boolean;
}; className?: string;
}
// eslint-disable-next-line unicorn/no-keyword-prefix export default function StatusCircle({
export default function StatusCircle({ isOnline, className }: Props) { isOnline,
className,
}: StatusCircleProps) {
return ( return (
<svg <svg
className={clsx( className={cn(
className,
isOnline isOnline
? 'text-green-700 dark:text-green-400' ? 'text-green-600 dark:text-green-500'
: 'text-gray-300 dark:text-gray-500', : 'text-headplane-200 dark:text-headplane-800',
className,
)} )}
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
> >
<title>{isOnline ? 'Online' : 'Offline'}</title>
<circle cx="12" cy="12" r="8" /> <circle cx="12" cy="12" r="8" />
</svg> </svg>
); );

View File

@ -1,11 +1,11 @@
import clsx from 'clsx';
import type { HTMLProps } from 'react'; import type { HTMLProps } from 'react';
import cn from '~/utils/cn';
function TableList(props: HTMLProps<HTMLDivElement>) { function TableList(props: HTMLProps<HTMLDivElement>) {
return ( return (
<div <div
{...props} {...props}
className={clsx( className={cn(
'rounded-xl', 'rounded-xl',
'border border-headplane-100 dark:border-headplane-800', 'border border-headplane-100 dark:border-headplane-800',
props.className, props.className,
@ -20,7 +20,7 @@ function Item(props: HTMLProps<HTMLDivElement>) {
return ( return (
<div <div
{...props} {...props}
className={clsx( className={cn(
'flex items-center justify-between p-2 last:border-b-0', 'flex items-center justify-between p-2 last:border-b-0',
'border-b border-headplane-100 dark:border-headplane-800', 'border-b border-headplane-100 dark:border-headplane-800',
props.className, props.className,

View File

@ -8,6 +8,6 @@ export interface TitleProps {
export default function Title({ children, className }: TitleProps) { export default function Title({ children, className }: TitleProps) {
return ( return (
<h3 className={cn('text-2xl font-bold mb-6', className)}>{children}</h3> <h3 className={cn('text-2xl font-bold mb-2', className)}>{children}</h3>
); );
} }

View File

@ -12,6 +12,7 @@ export interface TooltipProps extends AriaTooltipProps {
children: [React.ReactElement, React.ReactElement<TooltipBodyProps>]; children: [React.ReactElement, React.ReactElement<TooltipBodyProps>];
} }
// TODO: Fix Button accessibility outline + invoke
function Tooltip(props: TooltipProps) { function Tooltip(props: TooltipProps) {
const state = useTooltipTriggerState({ const state = useTooltipTriggerState({
...props, ...props,

View File

@ -7,15 +7,28 @@ interface Props {
export default function Fallback({ acl }: Props) { export default function Fallback({ acl }: Props) {
return ( return (
<div className="inline-block relative w-full h-editor"> <div className="relative w-full h-editor flex">
<Spinner className="w-4 h-4 absolute p-2" /> <div
className={cn(
'h-full w-8 flex justify-center p-1',
'border-r border-headscale-400 dark:border-headscale-800',
)}
>
<div
aria-hidden
className={cn(
'h-5 w-5 animate-spin rounded-full',
'border-headplane-900 dark:border-headplane-100',
'border-2 border-t-transparent dark:border-t-transparent',
)}
/>
</div>
<textarea <textarea
readOnly readOnly
className={cn( className={cn(
'w-full h-editor font-mono resize-none', 'w-full h-editor font-mono resize-none text-sm',
'text-sm text-gray-600 dark:text-gray-300', 'bg-headplane-50 dark:bg-headplane-950 opacity-60',
'bg-ui-100 dark:bg-ui-800', 'pl-1 pt-1 leading-snug',
'pl-10 pt-1 leading-snug',
)} )}
value={acl} value={acl}
/> />

View File

@ -90,7 +90,6 @@ export async function action({ request }: ActionFunctionArgs) {
export default function Page() { export default function Page() {
const data = useLoaderData<typeof loader>(); const data = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>(); const actionData = useActionData<typeof action>();
const showOr = useMemo(() => data.oidc && data.apiKey, [data]);
return ( return (
<div className="flex min-h-screen items-center justify-center"> <div className="flex min-h-screen items-center justify-center">
@ -98,7 +97,7 @@ export default function Page() {
<Card.Title>Welcome to Headplane</Card.Title> <Card.Title>Welcome to Headplane</Card.Title>
{data.apiKey ? ( {data.apiKey ? (
<Form method="post"> <Form method="post">
<Card.Text className="mb-8 text-sm"> <Card.Text>
Enter an API key to authenticate with Headplane. You can generate Enter an API key to authenticate with Headplane. You can generate
one by running <Code>headscale apikeys create</Code> in your one by running <Code>headscale apikeys create</Code> in your
terminal. terminal.
@ -109,27 +108,33 @@ export default function Page() {
) : undefined} ) : undefined}
<Input <Input
isRequired isRequired
labelHidden
label="API Key" label="API Key"
name="api-key" name="api-key"
placeholder="API Key" placeholder="API Key"
type="password" type="password"
className="mt-4 mb-2"
/> />
<Button className="w-full mt-2.5" variant="heavy" type="submit"> <Button className="w-full" variant="heavy" type="submit">
Sign In Sign In
</Button> </Button>
</Form> </Form>
) : undefined} ) : undefined}
{showOr ? (
<div className="flex items-center gap-x-1.5 py-1">
<hr className="flex-1 border-ui-300 dark:border-ui-800" />
<span className="text-gray-500 text-sm">or</span>
<hr className="flex-1 border-ui-300 dark:border-ui-800" />
</div>
) : undefined}
{data.oidc ? ( {data.oidc ? (
<Form method="POST"> <Form method="POST">
{!data.apiKey ? (
<Card.Text className="mb-6">
Sign in with your authentication provider to continue. Your
administrator has disabled API key login.
</Card.Text>
) : undefined}
<input type="hidden" name="oidc-start" value="true" /> <input type="hidden" name="oidc-start" value="true" />
<Button className="w-full" type="submit"> <Button
className="w-full mt-2"
variant={data.apiKey ? 'light' : 'heavy'}
type="submit"
>
Single Sign-On Single Sign-On
</Button> </Button>
</Form> </Form>

View File

@ -17,7 +17,7 @@ export default function DNS({ records, isDisabled }: Props) {
return ( return (
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">DNS Records</h1> <h1 className="text-2xl font-medium mb-4">DNS Records</h1>
<p className="text-gray-700 dark:text-gray-300"> <p>
Headscale supports adding custom DNS records to your Tailnet. As of now, Headscale supports adding custom DNS records to your Tailnet. As of now,
only <Code>A</Code> records are supported.{' '} only <Code>A</Code> records are supported.{' '}
<Link <Link

View File

@ -24,8 +24,7 @@ import cn from '~/utils/cn';
type Properties = { type Properties = {
readonly baseDomain?: string; readonly baseDomain?: string;
readonly searchDomains: string[]; readonly searchDomains: string[];
// eslint-disable-next-line react/boolean-prop-naming readonly disabled?: boolean; // TODO: isDisabled
readonly disabled?: boolean;
}; };
export default function Domains({ export default function Domains({
@ -33,7 +32,6 @@ export default function Domains({
searchDomains, searchDomains,
disabled, disabled,
}: Properties) { }: Properties) {
// eslint-disable-next-line unicorn/no-null, @typescript-eslint/ban-types
const [activeId, setActiveId] = useState<number | string | null>(null); const [activeId, setActiveId] = useState<number | string | null>(null);
const [localDomains, setLocalDomains] = useState(searchDomains); const [localDomains, setLocalDomains] = useState(searchDomains);
const [newDomain, setNewDomain] = useState(''); const [newDomain, setNewDomain] = useState('');
@ -46,7 +44,7 @@ export default function Domains({
return ( return (
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Search Domains</h1> <h1 className="text-2xl font-medium mb-4">Search Domains</h1>
<p className="text-gray-700 dark:text-gray-300 mb-2"> <p className="mb-4">
Set custom DNS search domains for your Tailnet. When using Magic DNS, Set custom DNS search domains for your Tailnet. When using Magic DNS,
your tailnet domain is used as the first search domain. your tailnet domain is used as the first search domain.
</p> </p>
@ -57,7 +55,6 @@ export default function Domains({
setActiveId(event.active.id); setActiveId(event.active.id);
}} }}
onDragEnd={(event) => { onDragEnd={(event) => {
// eslint-disable-next-line unicorn/no-null
setActiveId(null); setActiveId(null);
const { active, over } = event; const { active, over } = event;
if (!over) { if (!over) {
@ -82,7 +79,12 @@ export default function Domains({
<TableList> <TableList>
{baseDomain ? ( {baseDomain ? (
<TableList.Item key="magic-dns-sd"> <TableList.Item key="magic-dns-sd">
<div className="flex items-center gap-4"> <div
className={cn(
'flex items-center gap-4',
disabled ? 'flex-row-reverse justify-between w-full' : '',
)}
>
<Lock className="p-0.5" /> <Lock className="p-0.5" />
<p className="font-mono text-sm py-0.5">{baseDomain}</p> <p className="font-mono text-sm py-0.5">{baseDomain}</p>
</div> </div>
@ -138,7 +140,6 @@ export default function Domains({
onPress={() => { onPress={() => {
fetcher.submit( fetcher.submit(
{ {
// eslint-disable-next-line @typescript-eslint/naming-convention
'dns.search_domains': [...localDomains, newDomain], 'dns.search_domains': [...localDomains, newDomain],
}, },
{ {
@ -204,11 +205,6 @@ function Domain({
<p className="font-mono text-sm flex items-center gap-4"> <p className="font-mono text-sm flex items-center gap-4">
{disabled ? undefined : ( {disabled ? undefined : (
<GripVertical {...attributes} {...listeners} className="p-0.5" /> <GripVertical {...attributes} {...listeners} className="p-0.5" />
// <ThreeBarsIcon
// className="h-4 w-4 text-gray-400 focus:outline-none"
// {...attributes}
// {...listeners}
// />
)} )}
{domain} {domain}
</p> </p>

View File

@ -14,7 +14,7 @@ export default function Nameservers({ nameservers, isDisabled }: Props) {
return ( return (
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Nameservers</h1> <h1 className="text-2xl font-medium mb-4">Nameservers</h1>
<p className="text-gray-700 dark:text-gray-300"> <p>
Set the nameservers used by devices on the Tailnet to resolve DNS Set the nameservers used by devices on the Tailnet to resolve DNS
queries.{' '} queries.{' '}
<Link <Link

View File

@ -94,7 +94,7 @@ export default function Page() {
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Magic DNS</h1> <h1 className="text-2xl font-medium mb-4">Magic DNS</h1>
<p className="text-gray-700 dark:text-gray-300 mb-4"> <p className="mb-4">
Automatically register domain names for each device on the tailnet. Automatically register domain names for each device on the tailnet.
Devices will be accessible at{' '} Devices will be accessible at{' '}
<Code> <Code>

View File

@ -90,7 +90,7 @@ export default function MachineRow({
return ( return (
<tr <tr
key={machine.id} key={machine.id}
className="hover:bg-zinc-100 dark:hover:bg-zinc-800 group" className="group hover:bg-headplane-50 dark:hover:bg-headplane-950"
> >
<td className="pl-0.5 py-2"> <td className="pl-0.5 py-2">
<Link to={`/machines/${machine.id}`} className="group/link h-full"> <Link to={`/machines/${machine.id}`} className="group/link h-full">
@ -103,9 +103,7 @@ export default function MachineRow({
> >
{machine.givenName} {machine.givenName}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-300 font-mono"> <p className="text-sm font-mono opacity-50">{machine.name}</p>
{machine.name}
</p>
<div className="flex gap-1 mt-1"> <div className="flex gap-1 mt-1">
{tags.map((tag) => ( {tags.map((tag) => (
<Chip key={tag} text={tag} /> <Chip key={tag} text={tag} />
@ -152,12 +150,12 @@ export default function MachineRow({
<p className="leading-snug"> <p className="leading-snug">
{hinfo.getTSVersion(stats)} {hinfo.getTSVersion(stats)}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-300 max-w-48 truncate"> <p className="text-sm opacity-50 max-w-48 truncate">
{hinfo.getOSInfo(stats)} {hinfo.getOSInfo(stats)}
</p> </p>
</> </>
) : ( ) : (
<p className="text-sm text-gray-500 dark:text-gray-300"> <p className="text-sm opacity-50">
Unknown Unknown
</p> </p>
)} )}
@ -167,7 +165,7 @@ export default function MachineRow({
<span <span
className={cn( className={cn(
'flex items-center gap-x-1 text-sm', 'flex items-center gap-x-1 text-sm',
'text-gray-500 dark:text-gray-400', 'text-headplane-600 dark:text-headplane-300',
)} )}
> >
<StatusCircle <StatusCircle

View File

@ -23,7 +23,7 @@ export default function Rename({
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}> <Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Panel> <Dialog.Panel>
<Dialog.Title>Edit machine name for {machine.givenName}</Dialog.Title> <Dialog.Title>Edit machine name for {machine.givenName}</Dialog.Title>
<Dialog.Text> <Dialog.Text className="mb-6">
This name is shown in the admin panel, in Tailscale clients, and used This name is shown in the admin panel, in Tailscale clients, and used
when generating MagicDNS names. when generating MagicDNS names.
</Dialog.Text> </Dialog.Text>
@ -38,7 +38,7 @@ export default function Rename({
/> />
{magic ? ( {magic ? (
name.length > 0 && name !== machine.givenName ? ( name.length > 0 && name !== machine.givenName ? (
<p className="text-sm text-gray-500 dark:text-gray-300 leading-tight"> <p className="text-sm text-headplane-600 dark:text-headplane-300 leading-tight mt-2">
This machine will be accessible by the hostname{' '} This machine will be accessible by the hostname{' '}
<Code className="text-sm"> <Code className="text-sm">
{name.toLowerCase().replaceAll(/\s+/g, '-')} {name.toLowerCase().replaceAll(/\s+/g, '-')}
@ -48,7 +48,7 @@ export default function Rename({
will no longer point to this machine. will no longer point to this machine.
</p> </p>
) : ( ) : (
<p className="text-sm text-gray-500 dark:text-gray-300 leading-tight"> <p className="text-sm text-headplane-600 dark:text-headplane-300 leading-tight mt-2">
This machine is accessible by the hostname{' '} This machine is accessible by the hostname{' '}
<Code className="text-sm">{machine.givenName}</Code>. <Code className="text-sm">{machine.givenName}</Code>.
</p> </p>

View File

@ -1,8 +1,10 @@
import { GlobeLock, RouteOff } from 'lucide-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useFetcher } from 'react-router'; import { useFetcher } from 'react-router';
import Dialog from '~/components/Dialog'; import Dialog from '~/components/Dialog';
import Link from '~/components/Link'; import Link from '~/components/Link';
import Switch from '~/components/Switch'; import Switch from '~/components/Switch';
import TableList from '~/components/TableList';
import type { Machine, Route } from '~/types'; import type { Machine, Route } from '~/types';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
@ -59,32 +61,17 @@ export default function Routes({
Learn More Learn More
</Link> </Link>
</Dialog.Text> </Dialog.Text>
<div <TableList className="mt-4">
className={cn(
'rounded-lg overflow-y-auto my-2',
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top',
'border border-zinc-200 dark:border-zinc-700',
)}
>
{subnet.length === 0 ? ( {subnet.length === 0 ? (
<div <TableList.Item className="flex flex-col items-center gap-2.5 py-4 opacity-70">
className={cn( <RouteOff />
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800', <p className="font-semibold">
'items-center justify-center', No routes are advertised by this machine
'text-ui-600 dark:text-ui-300', </p>
)} </TableList.Item>
>
<p>No routes are advertised on this machine.</p>
</div>
) : undefined} ) : undefined}
{subnet.map((route) => ( {subnet.map((route) => (
<div <TableList.Item key={route.id}>
key={route.id}
className={cn(
'flex py-2 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-between',
)}
>
<p>{route.prefix}</p> <p>{route.prefix}</p>
<Switch <Switch
defaultSelected={route.enabled} defaultSelected={route.enabled}
@ -101,9 +88,9 @@ export default function Routes({
}); });
}} }}
/> />
</div> </TableList.Item>
))} ))}
</div> </TableList>
<Dialog.Text className="font-bold mt-8">Exit nodes</Dialog.Text> <Dialog.Text className="font-bold mt-8">Exit nodes</Dialog.Text>
<Dialog.Text> <Dialog.Text>
Allow your network to route internet traffic through this machine.{' '} Allow your network to route internet traffic through this machine.{' '}
@ -114,30 +101,14 @@ export default function Routes({
Learn More Learn More
</Link> </Link>
</Dialog.Text> </Dialog.Text>
<div <TableList className="mt-4">
className={cn(
'rounded-lg overflow-y-auto my-2',
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top',
'border border-zinc-200 dark:border-zinc-700',
)}
>
{exit.length === 0 ? ( {exit.length === 0 ? (
<div <TableList.Item className="flex flex-col items-center gap-2.5 py-4 opacity-70">
className={cn( <GlobeLock />
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800', <p className="font-semibold">This machine is not an exit node</p>
'items-center justify-center', </TableList.Item>
'text-ui-600 dark:text-ui-300',
)}
>
<p>This machine is not an exit node.</p>
</div>
) : ( ) : (
<div <TableList.Item>
className={cn(
'flex py-2 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-between',
)}
>
<p>Use as exit node</p> <p>Use as exit node</p>
<Switch <Switch
defaultSelected={exitEnabled} defaultSelected={exitEnabled}
@ -154,9 +125,9 @@ export default function Routes({
}); });
}} }}
/> />
</div> </TableList.Item>
)} )}
</div> </TableList>
</Dialog.Panel> </Dialog.Panel>
</Dialog> </Dialog>
); );

View File

@ -1,4 +1,4 @@
import { Plus, X } from 'lucide-react'; import { Plus, TagsIcon, X } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import Button from '~/components/Button'; import Button from '~/components/Button';
import Dialog from '~/components/Dialog'; import Dialog from '~/components/Dialog';
@ -38,15 +38,10 @@ export default function Tags({ machine, isOpen, setIsOpen }: TagsProps) {
<input type="hidden" name="tags" value={tags.join(',')} /> <input type="hidden" name="tags" value={tags.join(',')} />
<TableList className="mt-4"> <TableList className="mt-4">
{tags.length === 0 ? ( {tags.length === 0 ? (
<div <TableList.Item className="flex flex-col items-center gap-2.5 py-4 opacity-70">
className={cn( <TagsIcon />
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800', <p className="font-semibold">No tags are set on this machine</p>
'items-center justify-center rounded-t-lg', </TableList.Item>
'text-ui-600 dark:text-ui-300',
)}
>
<p>No tags are set on this machine.</p>
</div>
) : ( ) : (
tags.map((item) => ( tags.map((item) => (
<TableList.Item className="font-mono" key={item} id={item}> <TableList.Item className="font-mono" key={item} id={item}>

View File

@ -1,20 +1,12 @@
import { import { CheckCircle, CircleSlash, Info, UserCircle } from 'lucide-react';
CheckCircleIcon,
GearIcon,
InfoIcon,
PersonIcon,
SkipIcon,
} from '@primer/octicons-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 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';
import Chip from '~/components/Chip'; import Chip from '~/components/Chip';
import Link from '~/components/Link'; import Link from '~/components/Link';
import Menu from '~/components/Menu';
import StatusCircle from '~/components/StatusCircle'; import StatusCircle from '~/components/StatusCircle';
import Tooltip from '~/components/Tooltip'; import Tooltip from '~/components/Tooltip';
import type { Machine, Route, User } from '~/types'; import type { Machine, Route, User } from '~/types';
@ -23,7 +15,6 @@ import { loadContext } from '~/utils/config/headplane';
import { loadConfig } from '~/utils/config/headscale'; import { loadConfig } from '~/utils/config/headscale';
import { pull } from '~/utils/headscale'; import { pull } from '~/utils/headscale';
import { getSession } from '~/utils/sessions.server'; import { getSession } from '~/utils/sessions.server';
import { menuAction } from './action'; import { menuAction } from './action';
import MenuOptions from './components/menu'; import MenuOptions from './components/menu';
import Routes from './dialogs/routes'; import Routes from './dialogs/routes';
@ -126,11 +117,11 @@ export default function Page() {
</p> </p>
<div <div
className={cn( className={cn(
'flex justify-between items-center', 'flex justify-between items-center pb-2',
'border-b border-ui-100 dark:border-ui-800', 'border-b border-headplane-100 dark:border-headplane-800',
)} )}
> >
<span className="flex items-baseline gap-x-4 text-sm mb-4"> <span className="flex items-baseline gap-x-4 text-sm">
<h1 className="text-2xl font-medium">{machine.givenName}</h1> <h1 className="text-2xl font-medium">{machine.givenName}</h1>
<StatusCircle isOnline={machine.online} className="w-4 h-4" /> <StatusCircle isOnline={machine.online} className="w-4 h-4" />
</span> </span>
@ -144,30 +135,25 @@ export default function Page() {
/> />
</div> </div>
<div className="flex gap-1 mb-4"> <div className="flex gap-1 mb-4">
<div className="border-r border-ui-100 dark:border-ui-800 p-2 pr-4"> <div className="border-r border-headplane-100 dark:border-headplane-800 p-2 pr-4">
<span className="text-sm text-ui-600 dark:text-ui-300 flex items-center gap-x-1"> <span className="text-sm text-headplane-600 dark:text-headplane-300 flex items-center gap-x-1">
Managed by Managed by
<Tooltip> <Tooltip>
<InfoIcon className="w-3.5 h-3.5" /> <Info className="p-1" />
<Tooltip.Body> <Tooltip.Body>
By default, a machines permissions match its creators. By default, a machines permissions match its creators.
</Tooltip.Body> </Tooltip.Body>
</Tooltip> </Tooltip>
</span> </span>
<div className="flex items-center gap-x-2.5 mt-1"> <div className="flex items-center gap-x-2.5 mt-1">
<div <UserCircle />
className={cn(
'rounded-full h-7 w-7 flex items-center justify-center',
'border border-ui-200 dark:border-ui-700',
)}
>
<PersonIcon className="w-4 h-4" />
</div>
{machine.user.name} {machine.user.name}
</div> </div>
</div> </div>
<div className="p-2 pl-4"> <div className="p-2 pl-4">
<p className="text-sm text-ui-600 dark:text-ui-300">Status</p> <p className="text-sm text-headplane-600 dark:text-headplane-300">
Status
</p>
<div className="flex gap-1 mt-1 mb-8"> <div className="flex gap-1 mt-1 mb-8">
{tags.map((tag) => ( {tags.map((tag) => (
<Chip key={tag} text={tag} /> <Chip key={tag} text={tag} />
@ -202,10 +188,10 @@ export default function Page() {
)} )}
> >
<div> <div>
<span className="text-ui-600 dark:text-ui-300 flex items-center gap-x-1"> <span className="text-headplane-600 dark:text-headplane-300 flex items-center gap-x-1">
Approved Approved
<Tooltip> <Tooltip>
<InfoIcon className="w-3.5 h-3.5" /> <Info className="w-3.5 h-3.5" />
<Tooltip.Body> <Tooltip.Body>
Traffic to these routes are being routed through this machine. Traffic to these routes are being routed through this machine.
</Tooltip.Body> </Tooltip.Body>
@ -213,7 +199,7 @@ export default function Page() {
</span> </span>
<div className="mt-1"> <div className="mt-1">
{subnetApproved.length === 0 ? ( {subnetApproved.length === 0 ? (
<span className="text-ui-400 dark:text-ui-300"></span> <span className="opacity-50"></span>
) : ( ) : (
<ul className="leading-normal"> <ul className="leading-normal">
{subnetApproved.map((route) => ( {subnetApproved.map((route) => (
@ -233,10 +219,10 @@ export default function Page() {
</Button> </Button>
</div> </div>
<div> <div>
<span className="text-ui-600 dark:text-ui-300 flex items-center gap-x-1"> <span className="text-headplane-600 dark:text-headplane-300 flex items-center gap-x-1">
Awaiting Approval Awaiting Approval
<Tooltip> <Tooltip>
<InfoIcon className="w-3.5 h-3.5" /> <Info className="w-3.5 h-3.5" />
<Tooltip.Body> <Tooltip.Body>
This machine is advertising these routes, but they must be This machine is advertising these routes, but they must be
approved before traffic will be routed to them. approved before traffic will be routed to them.
@ -245,7 +231,7 @@ export default function Page() {
</span> </span>
<div className="mt-1"> <div className="mt-1">
{subnet.length === 0 ? ( {subnet.length === 0 ? (
<span className="text-ui-400 dark:text-ui-300"></span> <span className="opacity-50"></span>
) : ( ) : (
<ul className="leading-normal"> <ul className="leading-normal">
{subnet.map((route) => ( {subnet.map((route) => (
@ -265,10 +251,10 @@ export default function Page() {
</Button> </Button>
</div> </div>
<div> <div>
<span className="text-ui-600 dark:text-ui-300 flex items-center gap-x-1"> <span className="text-headplane-600 dark:text-headplane-300 flex items-center gap-x-1">
Exit Node Exit Node
<Tooltip> <Tooltip>
<InfoIcon className="w-3.5 h-3.5" /> <Info className="w-3.5 h-3.5" />
<Tooltip.Body> <Tooltip.Body>
Whether this machine can act as an exit node for your tailnet. Whether this machine can act as an exit node for your tailnet.
</Tooltip.Body> </Tooltip.Body>
@ -276,15 +262,15 @@ export default function Page() {
</span> </span>
<div className="mt-1"> <div className="mt-1">
{exit.length === 0 ? ( {exit.length === 0 ? (
<span className="text-ui-400 dark:text-ui-300"></span> <span className="opacity-50"></span>
) : exitEnabled ? ( ) : exitEnabled ? (
<span className="flex items-center gap-x-1"> <span className="flex items-center gap-x-1">
<CheckCircleIcon className="w-3.5 h-3.5 text-green-700" /> <CheckCircle className="w-3.5 h-3.5 text-green-700" />
Allowed Allowed
</span> </span>
) : ( ) : (
<span className="flex items-center gap-x-1"> <span className="flex items-center gap-x-1">
<SkipIcon className="w-3.5 h-3.5 text-red-700" /> <CircleSlash className="w-3.5 h-3.5 text-red-700" />
Awaiting Approval Awaiting Approval
</span> </span>
)} )}

View File

@ -59,10 +59,10 @@ export default function Page() {
return ( return (
<> <>
<div className="flex justify-between items-center mb-8"> <div className="flex justify-between items-center mb-6">
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Machines</h1> <h1 className="text-2xl font-medium mb-2">Machines</h1>
<p className="text-gray-700 dark:text-gray-300"> <p>
Manage the devices connected to your Tailnet.{' '} Manage the devices connected to your Tailnet.{' '}
<Link <Link
to="https://tailscale.com/kb/1372/manage-devices" to="https://tailscale.com/kb/1372/manage-devices"
@ -78,9 +78,9 @@ export default function Page() {
/> />
</div> </div>
<table className="table-auto w-full rounded-lg"> <table className="table-auto w-full rounded-lg">
<thead className="text-gray-500 dark:text-gray-400"> <thead className="text-headplane-600 dark:text-headplane-300">
<tr className="text-left px-0.5"> <tr className="text-left px-0.5">
<th className="pb-2">Name</th> <th className="uppercase text-xs font-bold pb-2">Name</th>
<th className="pb-2 w-1/4"> <th className="pb-2 w-1/4">
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
<p className="uppercase text-xs font-bold">Addresses</p> <p className="uppercase text-xs font-bold">Addresses</p>
@ -99,14 +99,14 @@ export default function Page() {
) : undefined} ) : undefined}
</div> </div>
</th> </th>
{/**<th className="pb-2">Version</th>**/} {/**<th className="uppercase text-xs font-bold pb-2">Version</th>**/}
<th className="pb-2">Last Seen</th> <th className="uppercase text-xs font-bold pb-2">Last Seen</th>
</tr> </tr>
</thead> </thead>
<tbody <tbody
className={cn( className={cn(
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top', 'divide-y divide-headplane-100 dark:divide-headplane-800 align-top',
'border-t border-zinc-200 dark:border-zinc-700', 'border-t border-headplane-100 dark:border-headplane-800',
)} )}
> >
{data.nodes.map((machine) => ( {data.nodes.map((machine) => (

View File

@ -119,31 +119,31 @@ export async function loader({ request }: LoaderFunctionArgs) {
export default function Page() { export default function Page() {
const { keys, users, server } = useLoaderData<typeof loader>(); const { keys, users, server } = useLoaderData<typeof loader>();
const [user, setUser] = useState('All'); const [user, setUser] = useState('__headplane_all');
const [status, setStatus] = useState('Active'); const [status, setStatus] = useState('active');
const filteredKeys = keys.filter((key) => { const filteredKeys = keys.filter((key) => {
if (user !== 'All' && key.user !== user) { if (user !== '__headplane_all' && key.user !== user) {
return false; return false;
} }
if (status !== 'All') { if (status !== 'all') {
const now = new Date(); const now = new Date();
const expiry = new Date(key.expiration); const expiry = new Date(key.expiration);
if (status === 'Active') { if (status === 'active') {
return !(expiry < now) && (!key.used || key.reusable); return !(expiry < now) && (!key.used || key.reusable);
} }
if (status === 'Used/Expired') { if (status === 'expired') {
return key.used || expiry < now; return key.used || expiry < now;
} }
if (status === 'Reusable') { if (status === 'reusable') {
return key.reusable; return key.reusable;
} }
if (status === 'Ephemeral') { if (status === 'ephemeral') {
return key.ephemeral; return key.ephemeral;
} }
} }
@ -160,8 +160,8 @@ export default function Page() {
</RemixLink> </RemixLink>
<span className="mx-2">/</span> Pre-Auth Keys <span className="mx-2">/</span> Pre-Auth Keys
</p> </p>
<h1 className="text-2xl font-medium mb-4">Pre-Auth Keys</h1> <h1 className="text-2xl font-medium mb-2">Pre-Auth Keys</h1>
<p className="text-gray-700 dark:text-gray-300 mb-4"> <p className="mb-4">
Headscale fully supports pre-authentication keys in order to easily add Headscale fully supports pre-authentication keys in order to easily add
devices to your Tailnet. To learn more about using pre-authentication devices to your Tailnet. To learn more about using pre-authentication
keys, visit the{' '} keys, visit the{' '}
@ -173,40 +173,34 @@ export default function Page() {
</Link> </Link>
</p> </p>
<AddPreAuthKey users={users} /> <AddPreAuthKey users={users} />
<div className="flex justify-between gap-4 mt-4"> <div className="flex items-center gap-4 mt-4">
<div className="w-full"> <Select
<p className="text-sm text-gray-500 dark:text-gray-300"> label="Filter by User"
Filter by user placeholder="Select a user"
</p> className="w-full"
<Select defaultSelectedKey="__headplane_all"
label="Filter by User" onSelectionChange={(value) => setUser(value?.toString() ?? '')}
placeholder="Select a user" >
onSelectionChange={(value) => setUser(value?.toString() ?? '')} {[
> <Select.Item key="__headplane_all">All</Select.Item>,
{[ ...users.map((user) => (
<Select.Item key="All">All</Select.Item>, <Select.Item key={user.name}>{user.name}</Select.Item>
...users.map((user) => ( )),
<Select.Item key={user.name}>{user.name}</Select.Item> ]}
)), </Select>
]} <Select
</Select> label="Filter by status"
</div> placeholder="Select a status"
<div className="w-full"> className="w-full"
<p className="text-sm text-gray-500 dark:text-gray-300"> defaultSelectedKey="active"
Filter by status onSelectionChange={(value) => setStatus(value?.toString() ?? '')}
</p> >
<Select <Select.Item key="all">All</Select.Item>
label="Filter by status" <Select.Item key="active">Active</Select.Item>
placeholder="Select a status" <Select.Item key="expired">Used/Expired</Select.Item>
defaultSelectedKey="Active" <Select.Item key="reusable">Reusable</Select.Item>
> <Select.Item key="ephemeral">Ephemeral</Select.Item>
<Select.Item key="All">All</Select.Item> </Select>
<Select.Item>Active</Select.Item>
<Select.Item>Used/Expired</Select.Item>
<Select.Item>Reusable</Select.Item>
<Select.Item>Ephemeral</Select.Item>
</Select>
</div>
</div> </div>
<TableList className="mt-4"> <TableList className="mt-4">
{filteredKeys.length === 0 ? ( {filteredKeys.length === 0 ? (

View File

@ -9,7 +9,7 @@ export default function AgentSection() {
<> <>
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Local Agent</h1> <h1 className="text-2xl font-medium mb-4">Local Agent</h1>
<p className="text-gray-700 dark:text-gray-300"> <p>
Headplane provides a local agent that can be installed on a server to Headplane provides a local agent that can be installed on a server to
provide additional features including viewing device information and provide additional features including viewing device information and
SSH access via the web interface (soon). To learn more about the agent SSH access via the web interface (soon). To learn more about the agent
@ -23,12 +23,7 @@ export default function AgentSection() {
</p> </p>
</div> </div>
<RemixLink to="/settings/local-agent"> <RemixLink to="/settings/local-agent">
<div <div className={cn('text-lg font-medium flex items-center')}>
className={cn(
'text-lg font-medium flex items-center',
'text-gray-700 dark:text-gray-300',
)}
>
Manage Agent Manage Agent
<ArrowRightIcon className="w-5 h-5 ml-2" /> <ArrowRightIcon className="w-5 h-5 ml-2" />
</div> </div>

View File

@ -1,5 +1,5 @@
import Card from '~/components/Card' import Card from '~/components/Card';
import StatusCircle from '~/components/StatusCircle' import StatusCircle from '~/components/StatusCircle';
import type { HostInfo } from '~/types'; import type { HostInfo } from '~/types';
import * as hinfo from '~/utils/host-info'; import * as hinfo from '~/utils/host-info';
@ -12,26 +12,21 @@ export default function AgentManagement({ reachable, hostInfo }: Props) {
console.log('hostInfo:', hostInfo); console.log('hostInfo:', hostInfo);
return ( return (
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4"> <h1 className="text-2xl font-medium mb-4">Local Agent Configuration</h1>
Local Agent Configuration <p className="mb-8">
</h1> A local agent has already been configured for this Headplane instance.
<p className="text-gray-700 dark:text-gray-300 mb-8"> You can manage the agent settings here.
A local agent has already been configured for this
Headplane instance. You can manage the agent settings here.
</p> </p>
<Card> <Card>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StatusCircle <StatusCircle isOnline={reachable} className="w-4 h-4 px-1" />
isOnline={reachable}
className="w-4 h-4 px-1 w-fit"
/>
<div> <div>
<p className="text-lg font-bold"> <p className="text-lg font-bold">
{hostInfo.Hostname ?? 'Unknown'} {hostInfo.Hostname ?? 'Unknown'}
</p> </p>
<p className="leading-snug"> <p className="leading-snug">
{hinfo.getTSVersion(hostInfo)} {hinfo.getTSVersion(hostInfo)}
<span className="ml-2 text-sm text-gray-500 dark:text-gray-300"> <span className="ml-2 text-sm text-headplane-600 dark:text-headplane-300">
{hinfo.getOSInfo(hostInfo)} {hinfo.getOSInfo(hostInfo)}
</span> </span>
</p> </p>
@ -40,5 +35,5 @@ export default function AgentManagement({ reachable, hostInfo }: Props) {
{JSON.stringify(hostInfo)} {JSON.stringify(hostInfo)}
</Card> </Card>
</div> </div>
) );
} }

View File

@ -11,7 +11,7 @@ export default function Page() {
<div className="flex flex-col gap-8 max-w-screen-lg"> <div className="flex flex-col gap-8 max-w-screen-lg">
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Settings</h1> <h1 className="text-2xl font-medium mb-4">Settings</h1>
<p className="text-gray-700 dark:text-gray-300"> <p>
The settings page is still under construction. As I'm able to add more The settings page is still under construction. As I'm able to add more
features, I'll be adding them here. If you require any features, feel features, I'll be adding them here. If you require any features, feel
free to open an issue on the GitHub repository. free to open an issue on the GitHub repository.
@ -19,7 +19,7 @@ export default function Page() {
</div> </div>
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Pre-Auth Keys</h1> <h1 className="text-2xl font-medium mb-4">Pre-Auth Keys</h1>
<p className="text-gray-700 dark:text-gray-300"> <p>
Headscale fully supports pre-authentication keys in order to easily Headscale fully supports pre-authentication keys in order to easily
add devices to your Tailnet. To learn more about using add devices to your Tailnet. To learn more about using
pre-authentication keys, visit the{' '} pre-authentication keys, visit the{' '}
@ -32,12 +32,7 @@ export default function Page() {
</p> </p>
</div> </div>
<RemixLink to="/settings/auth-keys"> <RemixLink to="/settings/auth-keys">
<div <div className="text-lg font-medium flex items-center">
className={cn(
'text-lg font-medium flex items-center',
'text-gray-700 dark:text-gray-300',
)}
>
Manage Auth Keys Manage Auth Keys
<ArrowRightIcon className="w-5 h-5 ml-2" /> <ArrowRightIcon className="w-5 h-5 ml-2" />
</div> </div>

View File

@ -13,10 +13,10 @@ export default function Auth({ magic }: Props) {
return ( return (
<Card variant="flat" className="mb-8 w-full max-w-full p-0"> <Card variant="flat" className="mb-8 w-full max-w-full p-0">
<div className="flex flex-col md:flex-row"> <div className="flex flex-col md:flex-row">
<div className="w-full p-4 border-b md:border-b-0 border-ui-200 dark:border-ui-700"> <div className="w-full p-4 border-b md:border-b-0 border-headplane-100 dark:border-headplane-800">
<HomeIcon className="w-5 h-5 mb-2" /> <HomeIcon className="w-5 h-5 mb-2" />
<h2 className="font-medium mb-1">Basic Authentication</h2> <h2 className="font-medium mb-1">Basic Authentication</h2>
<p className="text-sm text-ui-600 dark:text-ui-300"> <p className="text-sm text-headplane-600 dark:text-headplane-300">
Users are not managed externally. Using OpenID Connect can create a Users are not managed externally. Using OpenID Connect can create a
better experience when using Headscale.{' '} better experience when using Headscale.{' '}
<Link <Link
@ -27,10 +27,10 @@ export default function Auth({ magic }: Props) {
</Link> </Link>
</p> </p>
</div> </div>
<div className="w-full p-4 md:border-l border-ui-200 dark:border-ui-700"> <div className="w-full p-4 md:border-l border-headplane-100 dark:border-headplane-800">
<PasskeyFillIcon className="w-5 h-5 mb-2" /> <PasskeyFillIcon className="w-5 h-5 mb-2" />
<h2 className="font-medium mb-1">User Management</h2> <h2 className="font-medium mb-1">User Management</h2>
<p className="text-sm text-ui-600 dark:text-ui-300"> <p className="text-sm text-headplane-600 dark:text-headplane-300">
You can add, remove, and rename users here. You can add, remove, and rename users here.
</p> </p>
<div className="flex items-center gap-2 mt-4"> <div className="flex items-center gap-2 mt-4">

View File

@ -15,10 +15,10 @@ export default function Oidc({ oidc, magic }: Props) {
return ( return (
<Card variant="flat" className="mb-8 w-full max-w-full p-0"> <Card variant="flat" className="mb-8 w-full max-w-full p-0">
<div className="flex flex-col md:flex-row"> <div className="flex flex-col md:flex-row">
<div className="w-full p-4 border-b md:border-b-0 border-ui-200 dark:border-ui-700"> <div className="w-full p-4 border-b md:border-b-0 border-headplane-100 dark:border-headplane-800">
<OrganizationIcon className="w-5 h-5 mb-2" /> <OrganizationIcon className="w-5 h-5 mb-2" />
<h2 className="font-medium mb-1">OpenID Connect</h2> <h2 className="font-medium mb-1">OpenID Connect</h2>
<p className="text-sm text-ui-600 dark:text-ui-300"> <p className="text-sm text-headplane-600 dark:text-headplane-300">
Users are managed through your{' '} Users are managed through your{' '}
<Link to={oidc.issuer} name="OIDC Provider"> <Link to={oidc.issuer} name="OIDC Provider">
OpenID Connect provider OpenID Connect provider
@ -33,10 +33,10 @@ export default function Oidc({ oidc, magic }: Props) {
</Link> </Link>
</p> </p>
</div> </div>
<div className="w-full p-4 md:border-l border-ui-200 dark:border-ui-700"> <div className="w-full p-4 md:border-l border-headplane-100 dark:border-headplane-800">
<PasskeyFillIcon className="w-5 h-5 mb-2" /> <PasskeyFillIcon className="w-5 h-5 mb-2" />
<h2 className="font-medium mb-1">User Management</h2> <h2 className="font-medium mb-1">User Management</h2>
<p className="text-sm text-ui-600 dark:text-ui-300"> <p className="text-sm text-headplane-600 dark:text-headplane-300">
You can still add users manually, however it is recommended that you You can still add users manually, however it is recommended that you
manage users through your OIDC provider. manage users through your OIDC provider.
</p> </p>

View File

@ -253,7 +253,7 @@ function MachineChip({ machine }: { readonly machine: Machine }) {
ref={setNodeRef} ref={setNodeRef}
className={cn( className={cn(
'flex items-center w-full gap-2 py-1', 'flex items-center w-full gap-2 py-1',
'hover:bg-ui-100 dark:hover:bg-ui-800 rounded-lg', 'hover:bg-headplane-50 dark:hover:bg-headplane-950 rounded-xl',
)} )}
style={{ style={{
transform: transform transform: transform
@ -263,7 +263,7 @@ function MachineChip({ machine }: { readonly machine: Machine }) {
{...listeners} {...listeners}
{...attributes} {...attributes}
> >
<StatusCircle isOnline={machine.online} className="w-4 h-4 px-1 w-fit" /> <StatusCircle isOnline={machine.online} className="px-1 h-4 w-fit" />
<Attribute <Attribute
name={machine.givenName} name={machine.givenName}
link={`machines/${machine.id}`} link={`machines/${machine.id}`}
@ -289,7 +289,7 @@ function UserCard({ user, magic }: CardProps) {
variant="flat" variant="flat"
className={cn( className={cn(
'max-w-full w-full overflow-visible h-full', 'max-w-full w-full overflow-visible h-full',
isOver ? 'bg-ui-100 dark:bg-ui-800' : '', isOver ? 'bg-headplane-100 dark:bg-headplane-800' : '',
)} )}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@ -2,18 +2,17 @@
// Functionally only used for all sorts of sanity checks across headplane. // Functionally only used for all sorts of sanity checks across headplane.
// //
// Around the codebase, this is referred to as the context // Around the codebase, this is referred to as the context
// TODO: Fix the TRASH that is this env var mess
// - Zod needs to be used for the config
// - Switch to YAML for the config file
import { access, constants, readFile, writeFile } from 'node:fs/promises'; import { constants, access, readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { parse } from 'yaml';
import { IntegrationFactory, loadIntegration } from '~/integration'; import { IntegrationFactory, loadIntegration } from '~/integration';
import { HeadscaleConfig, loadConfig } from '~/utils/config/headscale'; import { HeadscaleConfig, loadConfig } from '~/utils/config/headscale';
import { testOidc } from '~/utils/oidc';
import log from '~/utils/log'; import log from '~/utils/log';
import { testOidc } from '~/utils/oidc';
import { initSessionManager } from '~/utils/sessions.server'; import { initSessionManager } from '~/utils/sessions.server';
import { initAgentCache } from '~/utils/ws-agent';
export interface HeadplaneContext { export interface HeadplaneContext {
debug: boolean; debug: boolean;
@ -26,7 +25,7 @@ export interface HeadplaneContext {
enabled: boolean; enabled: boolean;
path: string; path: string;
defaultTTL: number; defaultTTL: number;
} };
config: { config: {
read: boolean; read: boolean;
@ -107,7 +106,8 @@ export async function loadContext(): Promise<HeadplaneContext> {
initSessionManager(); initSessionManager();
const cacheEnabled = process.env.AGENT_CACHE_DISABLED !== 'true'; const cacheEnabled = process.env.AGENT_CACHE_DISABLED !== 'true';
const cachePath = process.env.AGENT_CACHE_PATH ?? '/etc/headplane/agent.cache'; const cachePath =
process.env.AGENT_CACHE_PATH ?? '/etc/headplane/agent.cache';
const cacheTTL = 300 * 1000; // 5 minutes const cacheTTL = 300 * 1000; // 5 minutes
// Load agent cache // Load agent cache
@ -237,9 +237,9 @@ async function checkOidc(config?: HeadscaleConfig) {
clientId: client, clientId: client,
clientSecret: secret, clientSecret: secret,
tokenEndpointAuthMethod: method, tokenEndpointAuthMethod: method,
} };
const result = await testOidc(oidcConfig) const result = await testOidc(oidcConfig);
if (!result) { if (!result) {
return; return;
} }
@ -301,9 +301,9 @@ async function checkOidc(config?: HeadscaleConfig) {
clientId: client, clientId: client,
clientSecret: secret, clientSecret: secret,
tokenEndpointAuthMethod: method, tokenEndpointAuthMethod: method,
} };
const result = await testOidc(oidcConfig) const result = await testOidc(oidcConfig);
if (!result) { if (!result) {
return; return;
} }