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);
}}
>
{value}
<p className="truncate">{value}</p>
<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" />
</button>

View File

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

View File

@ -97,7 +97,10 @@ function MenuSection<T>({ section, state }: MenuSectionProps<T>) {
{section.key !== state.collection.getFirstKey() ? (
<li
{...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}
<li {...itemProps}>

View File

@ -1,23 +1,16 @@
import { InfoIcon } from '@primer/octicons-react';
import type { ReactNode } from 'react';
import cn from '~/utils/cn';
import { CircleSlash2 } from 'lucide-react';
import React from 'react';
import Card from '~/components/Card';
interface Props {
className?: string;
children: ReactNode;
export interface NoticeProps {
children: React.ReactNode;
}
export default function Notice({ children, className }: Props) {
export default function Notice({ children }: NoticeProps) {
return (
<div
className={cn(
'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" />
<Card className="flex w-full max-w-full gap-4 font-semibold">
<CircleSlash2 />
{children}
</div>
</Card>
);
}

View File

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

View File

@ -6,14 +6,13 @@ interface Props {
export default function Spinner({ className }: Props) {
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
className={clsx(
'animate-spin rounded-full w-full h-full',
'border-2 border-current border-t-transparent',
className,
)}
role="status"
>
<span className="sr-only">Loading...</span>
</div>

View File

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

View File

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

View File

@ -8,6 +8,6 @@ export interface TitleProps {
export default function Title({ children, className }: TitleProps) {
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>];
}
// TODO: Fix Button accessibility outline + invoke
function Tooltip(props: TooltipProps) {
const state = useTooltipTriggerState({
...props,

View File

@ -7,15 +7,28 @@ interface Props {
export default function Fallback({ acl }: Props) {
return (
<div className="inline-block relative w-full h-editor">
<Spinner className="w-4 h-4 absolute p-2" />
<div className="relative w-full h-editor flex">
<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
readOnly
className={cn(
'w-full h-editor font-mono resize-none',
'text-sm text-gray-600 dark:text-gray-300',
'bg-ui-100 dark:bg-ui-800',
'pl-10 pt-1 leading-snug',
'w-full h-editor font-mono resize-none text-sm',
'bg-headplane-50 dark:bg-headplane-950 opacity-60',
'pl-1 pt-1 leading-snug',
)}
value={acl}
/>

View File

@ -90,7 +90,6 @@ export async function action({ request }: ActionFunctionArgs) {
export default function Page() {
const data = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const showOr = useMemo(() => data.oidc && data.apiKey, [data]);
return (
<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>
{data.apiKey ? (
<Form method="post">
<Card.Text className="mb-8 text-sm">
<Card.Text>
Enter an API key to authenticate with Headplane. You can generate
one by running <Code>headscale apikeys create</Code> in your
terminal.
@ -109,27 +108,33 @@ export default function Page() {
) : undefined}
<Input
isRequired
labelHidden
label="API Key"
name="api-key"
placeholder="API Key"
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
</Button>
</Form>
) : 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 ? (
<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" />
<Button className="w-full" type="submit">
<Button
className="w-full mt-2"
variant={data.apiKey ? 'light' : 'heavy'}
type="submit"
>
Single Sign-On
</Button>
</Form>

View File

@ -17,7 +17,7 @@ export default function DNS({ records, isDisabled }: Props) {
return (
<div className="flex flex-col w-2/3">
<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,
only <Code>A</Code> records are supported.{' '}
<Link

View File

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

View File

@ -14,7 +14,7 @@ export default function Nameservers({ nameservers, isDisabled }: Props) {
return (
<div className="flex flex-col w-2/3">
<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
queries.{' '}
<Link

View File

@ -94,7 +94,7 @@ export default function Page() {
<div className="flex flex-col w-2/3">
<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.
Devices will be accessible at{' '}
<Code>

View File

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

View File

@ -23,7 +23,7 @@ export default function Rename({
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Panel>
<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
when generating MagicDNS names.
</Dialog.Text>
@ -38,7 +38,7 @@ export default function Rename({
/>
{magic ? (
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{' '}
<Code className="text-sm">
{name.toLowerCase().replaceAll(/\s+/g, '-')}
@ -48,7 +48,7 @@ export default function Rename({
will no longer point to this machine.
</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{' '}
<Code className="text-sm">{machine.givenName}</Code>.
</p>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import Card from '~/components/Card'
import StatusCircle from '~/components/StatusCircle'
import Card from '~/components/Card';
import StatusCircle from '~/components/StatusCircle';
import type { HostInfo } from '~/types';
import * as hinfo from '~/utils/host-info';
@ -12,26 +12,21 @@ export default function AgentManagement({ reachable, hostInfo }: Props) {
console.log('hostInfo:', hostInfo);
return (
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">
Local Agent Configuration
</h1>
<p className="text-gray-700 dark:text-gray-300 mb-8">
A local agent has already been configured for this
Headplane instance. You can manage the agent settings here.
<h1 className="text-2xl font-medium mb-4">Local Agent Configuration</h1>
<p className="mb-8">
A local agent has already been configured for this Headplane instance.
You can manage the agent settings here.
</p>
<Card>
<div className="flex items-center gap-2">
<StatusCircle
isOnline={reachable}
className="w-4 h-4 px-1 w-fit"
/>
<StatusCircle isOnline={reachable} className="w-4 h-4 px-1" />
<div>
<p className="text-lg font-bold">
{hostInfo.Hostname ?? 'Unknown'}
</p>
<p className="leading-snug">
{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)}
</span>
</p>
@ -40,5 +35,5 @@ export default function AgentManagement({ reachable, hostInfo }: Props) {
{JSON.stringify(hostInfo)}
</Card>
</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 w-2/3">
<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
features, I'll be adding them here. If you require any features, feel
free to open an issue on the GitHub repository.
@ -19,7 +19,7 @@ export default function Page() {
</div>
<div className="flex flex-col w-2/3">
<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
add devices to your Tailnet. To learn more about using
pre-authentication keys, visit the{' '}
@ -32,12 +32,7 @@ export default function Page() {
</p>
</div>
<RemixLink to="/settings/auth-keys">
<div
className={cn(
'text-lg font-medium flex items-center',
'text-gray-700 dark:text-gray-300',
)}
>
<div className="text-lg font-medium flex items-center">
Manage Auth Keys
<ArrowRightIcon className="w-5 h-5 ml-2" />
</div>

View File

@ -13,10 +13,10 @@ export default function Auth({ magic }: Props) {
return (
<Card variant="flat" className="mb-8 w-full max-w-full p-0">
<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" />
<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
better experience when using Headscale.{' '}
<Link
@ -27,10 +27,10 @@ export default function Auth({ magic }: Props) {
</Link>
</p>
</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" />
<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.
</p>
<div className="flex items-center gap-2 mt-4">

View File

@ -15,10 +15,10 @@ export default function Oidc({ oidc, magic }: Props) {
return (
<Card variant="flat" className="mb-8 w-full max-w-full p-0">
<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" />
<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{' '}
<Link to={oidc.issuer} name="OIDC Provider">
OpenID Connect provider
@ -33,10 +33,10 @@ export default function Oidc({ oidc, magic }: Props) {
</Link>
</p>
</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" />
<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
manage users through your OIDC provider.
</p>

View File

@ -253,7 +253,7 @@ function MachineChip({ machine }: { readonly machine: Machine }) {
ref={setNodeRef}
className={cn(
'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={{
transform: transform
@ -263,7 +263,7 @@ function MachineChip({ machine }: { readonly machine: Machine }) {
{...listeners}
{...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
name={machine.givenName}
link={`machines/${machine.id}`}
@ -289,7 +289,7 @@ function UserCard({ user, magic }: CardProps) {
variant="flat"
className={cn(
'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">

View File

@ -2,18 +2,17 @@
// Functionally only used for all sorts of sanity checks across headplane.
//
// 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 { parse } from 'yaml';
import { IntegrationFactory, loadIntegration } from '~/integration';
import { HeadscaleConfig, loadConfig } from '~/utils/config/headscale';
import { testOidc } from '~/utils/oidc';
import log from '~/utils/log';
import { testOidc } from '~/utils/oidc';
import { initSessionManager } from '~/utils/sessions.server';
import { initAgentCache } from '~/utils/ws-agent';
export interface HeadplaneContext {
debug: boolean;
@ -26,7 +25,7 @@ export interface HeadplaneContext {
enabled: boolean;
path: string;
defaultTTL: number;
}
};
config: {
read: boolean;
@ -107,7 +106,8 @@ export async function loadContext(): Promise<HeadplaneContext> {
initSessionManager();
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
// Load agent cache
@ -237,9 +237,9 @@ async function checkOidc(config?: HeadscaleConfig) {
clientId: client,
clientSecret: secret,
tokenEndpointAuthMethod: method,
}
};
const result = await testOidc(oidcConfig)
const result = await testOidc(oidcConfig);
if (!result) {
return;
}
@ -301,9 +301,9 @@ async function checkOidc(config?: HeadscaleConfig) {
clientId: client,
clientSecret: secret,
tokenEndpointAuthMethod: method,
}
};
const result = await testOidc(oidcConfig)
const result = await testOidc(oidcConfig);
if (!result) {
return;
}