feat: unify all colors
This commit is contained in:
parent
d1f6c450c0
commit
287ac2dff0
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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 machine’s permissions match its creator’s.
|
||||
</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>
|
||||
)}
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user