mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
feat: update some components
This commit is contained in:
@@ -24,9 +24,8 @@ export default function Attribute({ name, value, tooltip, isCopyable }: Attribut
|
||||
>
|
||||
{name}
|
||||
{tooltip ? (
|
||||
<Tooltip>
|
||||
<Tooltip content={tooltip}>
|
||||
<Info className="size-4" />
|
||||
<Tooltip.Body>{tooltip}</Tooltip.Body>
|
||||
</Tooltip>
|
||||
) : undefined}
|
||||
</dt>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function Code({ isCopyable, children, className }: CodeProps) {
|
||||
<button
|
||||
className="absolute right-0 bottom-0"
|
||||
onClick={async (event) => {
|
||||
const text = Array.isArray(children) ? children.join("") : children;
|
||||
const text = Array.isArray(children) ? children.join("") : String(children);
|
||||
|
||||
const svgs = event.currentTarget.querySelectorAll("svg");
|
||||
for (const svg of svgs) {
|
||||
|
||||
+55
-52
@@ -1,86 +1,89 @@
|
||||
import { useRef } from "react";
|
||||
import { AriaTabListProps, AriaTabPanelProps, useTab, useTabList, useTabPanel } from "react-aria";
|
||||
import { Item, Node, TabListState, useTabListState } from "react-stately";
|
||||
import { Tabs as BaseTabs } from "@base-ui/react/tabs";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
export interface TabsProps extends AriaTabListProps<object> {
|
||||
export interface TabsProps {
|
||||
label: string;
|
||||
className?: string;
|
||||
defaultValue?: string | number;
|
||||
value?: string | number;
|
||||
onValueChange?: (value: string | number) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function Tabs({ label, className, ...props }: TabsProps) {
|
||||
const state = useTabListState(props);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { tabListProps } = useTabList(props, state, ref);
|
||||
function Tabs({ label, className, children, ...props }: TabsProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col", className)}>
|
||||
<div
|
||||
{...tabListProps}
|
||||
className={cn(
|
||||
"flex items-center rounded-t-lg w-fit max-w-full overflow-x-auto",
|
||||
"border-mist-100 dark:border-mist-800",
|
||||
"border-t border-x",
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<Tab item={item} key={item.key} state={state} />
|
||||
))}
|
||||
</div>
|
||||
<TabsPanel key={state.selectedItem?.key} state={state} />
|
||||
</div>
|
||||
<BaseTabs.Root
|
||||
{...props}
|
||||
defaultValue={props.defaultValue ?? 0}
|
||||
aria-label={label}
|
||||
className={cn("flex flex-col", className)}
|
||||
>
|
||||
{children}
|
||||
</BaseTabs.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export interface TabsTabProps {
|
||||
item: Node<object>;
|
||||
state: TabListState<object>;
|
||||
function TabList({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<BaseTabs.List
|
||||
className={cn(
|
||||
"flex items-center rounded-t-lg w-fit max-w-full",
|
||||
"border-mist-100 dark:border-mist-800",
|
||||
"border-t border-x",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</BaseTabs.List>
|
||||
);
|
||||
}
|
||||
|
||||
function Tab({ item, state }: TabsTabProps) {
|
||||
const { key, rendered } = item;
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { tabProps } = useTab({ key }, state, ref);
|
||||
function Tab({
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof BaseTabs.Tab> & { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
{...tabProps}
|
||||
<BaseTabs.Tab
|
||||
value={value}
|
||||
{...props}
|
||||
className={cn(
|
||||
"pl-2 pr-3 py-2.5",
|
||||
"aria-selected:bg-mist-100 dark:aria-selected:bg-mist-950",
|
||||
"data-[selected]:bg-mist-100 dark:data-[selected]:bg-mist-950",
|
||||
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 z-10",
|
||||
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
|
||||
"border-r border-mist-100 dark:border-mist-800",
|
||||
"first:rounded-tl-lg last:rounded-tr-lg last:border-r-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
{rendered}
|
||||
</div>
|
||||
{children}
|
||||
</BaseTabs.Tab>
|
||||
);
|
||||
}
|
||||
|
||||
export interface TabsPanelProps extends AriaTabPanelProps {
|
||||
state: TabListState<object>;
|
||||
}
|
||||
|
||||
function TabsPanel({ state, ...props }: TabsPanelProps) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const { tabPanelProps } = useTabPanel(props, state, ref);
|
||||
function Panel({
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof BaseTabs.Panel> & { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
{...tabPanelProps}
|
||||
<BaseTabs.Panel
|
||||
value={value}
|
||||
{...props}
|
||||
className={cn(
|
||||
"w-full overflow-clip rounded-b-lg rounded-r-lg",
|
||||
"border border-mist-100 dark:border-mist-800",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
{state.selectedItem?.props.children}
|
||||
</div>
|
||||
{children}
|
||||
</BaseTabs.Panel>
|
||||
);
|
||||
}
|
||||
|
||||
export default Object.assign(Tabs, { Item });
|
||||
export { Tabs, TabList as TabsList, Tab as TabsTab, Panel as TabsPanel };
|
||||
|
||||
@@ -11,22 +11,23 @@ export interface ExitNodeTagProps {
|
||||
|
||||
export function ExitNodeTag({ isEnabled }: ExitNodeTagProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<Chip
|
||||
text="Exit Node"
|
||||
className={cn("bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-300")}
|
||||
rightIcon={isEnabled ? undefined : <Info className="h-full w-fit" />}
|
||||
/>
|
||||
<Tooltip.Body>
|
||||
{isEnabled ? (
|
||||
<Tooltip
|
||||
content={
|
||||
isEnabled ? (
|
||||
<>This machine is acting as an exit node.</>
|
||||
) : (
|
||||
<>
|
||||
This machine is requesting to be used as an exit node. Review this from the "Edit route
|
||||
settings..." option in the machine's menu.
|
||||
</>
|
||||
)}
|
||||
</Tooltip.Body>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Chip
|
||||
text="Exit Node"
|
||||
className={cn("bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-300")}
|
||||
rightIcon={isEnabled ? undefined : <Info className="h-full w-fit" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,23 +14,24 @@ export function ExpiryTag({ variant, expiry }: ExpiryTagProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<Chip
|
||||
text={
|
||||
variant === "expired" ? `Expired ${formatter.format(new Date(expiry!))}` : "No expiry"
|
||||
}
|
||||
className="bg-mist-200 text-mist-800 dark:bg-mist-800 dark:text-mist-200"
|
||||
/>
|
||||
<Tooltip.Body>
|
||||
{variant === "expired" ? (
|
||||
<Tooltip
|
||||
content={
|
||||
variant === "expired" ? (
|
||||
<>
|
||||
This machine is expired and will not be able to connect to the network. Re-authenticate
|
||||
with Tailscale on the machine to re-enable it.
|
||||
</>
|
||||
) : (
|
||||
<>This machine has key expiry disabled and will never need to re-authenticate.</>
|
||||
)}
|
||||
</Tooltip.Body>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Chip
|
||||
text={
|
||||
variant === "expired" ? `Expired ${formatter.format(new Date(expiry!))}` : "No expiry"
|
||||
}
|
||||
className="bg-mist-200 text-mist-800 dark:bg-mist-800 dark:text-mist-200"
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,15 +5,11 @@ import Tooltip from "../tooltip";
|
||||
|
||||
export function HeadplaneAgentTag() {
|
||||
return (
|
||||
<Tooltip>
|
||||
<Tooltip content="This machine is running the Headplane agent, which allows it to provide host information in the web UI.">
|
||||
<Chip
|
||||
text="Headplane Agent"
|
||||
className={cn("bg-purple-300 text-purple-900 dark:bg-purple-900 dark:text-purple-300")}
|
||||
/>
|
||||
<Tooltip.Body>
|
||||
This machine is running the Headplane agent, which allows it to provide host information in
|
||||
the web UI.
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,22 +11,23 @@ export interface SubnetTagProps {
|
||||
|
||||
export function SubnetTag({ isEnabled }: SubnetTagProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<Chip
|
||||
text="Subnets"
|
||||
className={cn("bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-300")}
|
||||
rightIcon={isEnabled ? undefined : <Info className="h-full w-fit" />}
|
||||
/>
|
||||
<Tooltip.Body>
|
||||
{isEnabled ? (
|
||||
<Tooltip
|
||||
content={
|
||||
isEnabled ? (
|
||||
<>This machine advertises subnet routes.</>
|
||||
) : (
|
||||
<>
|
||||
This machine has unadvertised subnet routes. Review this from the "Edit route
|
||||
settings..." option in the machine's menu.
|
||||
</>
|
||||
)}
|
||||
</Tooltip.Body>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Chip
|
||||
text="Subnets"
|
||||
className={cn("bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-300")}
|
||||
rightIcon={isEnabled ? undefined : <Info className="h-full w-fit" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,15 +5,11 @@ import Tooltip from "../tooltip";
|
||||
|
||||
export function TailscaleSSHTag() {
|
||||
return (
|
||||
<Tooltip>
|
||||
<Tooltip content="This machine advertises Tailscale SSH, which allows you to authenticate SSH credentials using your Tailscale account and via the Headplane web UI.">
|
||||
<Chip
|
||||
text="Tailscale SSH"
|
||||
className={cn("bg-lime-500 text-lime-900 dark:bg-lime-900 dark:text-lime-500")}
|
||||
/>
|
||||
<Tooltip.Body>
|
||||
This machine advertises Tailscale SSH, which allows you to authenticate SSH credentials
|
||||
using your Tailscale account and via the Headplane web UI.
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
+36
-71
@@ -1,80 +1,45 @@
|
||||
import React, { cloneElement, useRef } from "react";
|
||||
import { AriaTooltipProps, mergeProps, useTooltip, useTooltipTrigger } from "react-aria";
|
||||
import { TooltipTriggerState, useTooltipTriggerState } from "react-stately";
|
||||
import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
export interface TooltipProps extends AriaTooltipProps {
|
||||
children: [React.ReactElement, React.ReactElement<TooltipBodyProps>];
|
||||
}
|
||||
|
||||
function Tooltip(props: TooltipProps) {
|
||||
const state = useTooltipTriggerState({
|
||||
...props,
|
||||
delay: 0,
|
||||
closeDelay: 0,
|
||||
});
|
||||
|
||||
const ref = useRef<HTMLButtonElement | null>(null);
|
||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||
{
|
||||
...props,
|
||||
delay: 0,
|
||||
closeDelay: 0,
|
||||
},
|
||||
state,
|
||||
ref,
|
||||
);
|
||||
|
||||
const [component, body] = props.children;
|
||||
return (
|
||||
<span className="relative">
|
||||
<button
|
||||
ref={ref}
|
||||
{...triggerProps}
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 rounded-md",
|
||||
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
|
||||
)}
|
||||
>
|
||||
{component}
|
||||
</button>
|
||||
{state.isOpen &&
|
||||
cloneElement(body, {
|
||||
...tooltipProps,
|
||||
state,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface TooltipBodyProps extends AriaTooltipProps {
|
||||
children: React.ReactNode;
|
||||
state?: TooltipTriggerState;
|
||||
export interface TooltipProps {
|
||||
children: ReactNode;
|
||||
content: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Body({ state, className, ...props }: TooltipBodyProps) {
|
||||
const { tooltipProps } = useTooltip(props, state);
|
||||
export default function Tooltip({ children, content, className }: TooltipProps) {
|
||||
return (
|
||||
<span
|
||||
{...mergeProps(props, tooltipProps)}
|
||||
className={cn(
|
||||
"absolute z-50 p-3 top-full mt-1",
|
||||
"outline-hidden rounded-lg text-sm w-48",
|
||||
"bg-white dark:bg-mist-950",
|
||||
"text-black dark:text-white",
|
||||
"shadow-overlay",
|
||||
"border border-mist-100 dark:border-mist-800",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
<BaseTooltip.Root>
|
||||
<BaseTooltip.Trigger
|
||||
delay={0}
|
||||
closeDelay={0}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-md",
|
||||
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1",
|
||||
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</BaseTooltip.Trigger>
|
||||
<BaseTooltip.Portal>
|
||||
<BaseTooltip.Positioner sideOffset={4}>
|
||||
<BaseTooltip.Popup
|
||||
className={cn(
|
||||
"z-50 rounded-lg p-3 text-sm w-48",
|
||||
"outline-hidden",
|
||||
"bg-white dark:bg-mist-950",
|
||||
"text-black dark:text-white",
|
||||
"shadow-overlay",
|
||||
"border border-mist-100 dark:border-mist-800",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</BaseTooltip.Popup>
|
||||
</BaseTooltip.Positioner>
|
||||
</BaseTooltip.Portal>
|
||||
</BaseTooltip.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export default Object.assign(Tooltip, {
|
||||
Body,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import Code from "~/components/code";
|
||||
import Link from "~/components/link";
|
||||
import Notice from "~/components/notice";
|
||||
import PageError from "~/components/page-error";
|
||||
import Tabs from "~/components/tabs";
|
||||
import { Tabs, TabsList, TabsPanel, TabsTab } from "~/components/tabs";
|
||||
import { isApiError } from "~/server/headscale/api/error-client";
|
||||
import toast from "~/utils/toast";
|
||||
|
||||
@@ -79,38 +79,34 @@ export default function Page({ loaderData: { access, writable, policy } }: Route
|
||||
"An unknown error occurred while trying to update the ACL policy."}
|
||||
</Notice>
|
||||
) : undefined}
|
||||
<Tabs className="mb-4" label="ACL Editor">
|
||||
<Tabs.Item
|
||||
key="edit"
|
||||
title={
|
||||
<Tabs className="mb-4" label="ACL Editor" defaultValue="edit">
|
||||
<TabsList>
|
||||
<TabsTab value="edit">
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="p-1" />
|
||||
<span>Edit file</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Editor isDisabled={disabled} onChange={setCodePolicy} value={codePolicy} />
|
||||
</Tabs.Item>
|
||||
<Tabs.Item
|
||||
key="diff"
|
||||
title={
|
||||
</TabsTab>
|
||||
<TabsTab value="diff">
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="p-1" />
|
||||
<span>Preview changes</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Differ left={policy} right={codePolicy} />
|
||||
</Tabs.Item>
|
||||
<Tabs.Item
|
||||
key="preview"
|
||||
title={
|
||||
</TabsTab>
|
||||
<TabsTab value="preview">
|
||||
<div className="flex items-center gap-2">
|
||||
<FlaskConical className="p-1" />
|
||||
<span>Preview rules</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
</TabsTab>
|
||||
</TabsList>
|
||||
<TabsPanel value="edit">
|
||||
<Editor isDisabled={disabled} onChange={setCodePolicy} value={codePolicy} />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="diff">
|
||||
<Differ left={policy} right={codePolicy} />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="preview">
|
||||
<div className="flex flex-col items-center py-8">
|
||||
<Construction />
|
||||
<p className="mt-4 w-1/2 text-center">
|
||||
@@ -118,7 +114,7 @@ export default function Page({ loaderData: { access, writable, policy } }: Route
|
||||
pretty complicated to implement. Hopefully I will be able to get to it soon.
|
||||
</p>
|
||||
</div>
|
||||
</Tabs.Item>
|
||||
</TabsPanel>
|
||||
</Tabs>
|
||||
<Button
|
||||
className="mr-2"
|
||||
|
||||
@@ -67,16 +67,19 @@ function NameserverList({ isGlobal, isDisabled, nameservers, overrideLocalDns, n
|
||||
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-md font-medium opacity-80">Global Nameservers</h2>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
When enabled, use the DNS servers listed below to resolve names outside the
|
||||
tailnet. When disabled (default), devices will prefer their local DNS
|
||||
configuration.{" "}
|
||||
<Link external styled to="https://tailscale.com/kb/1054/dns#global-nameservers">
|
||||
Learn More
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Info className="size-4" />
|
||||
<Tooltip.Body>
|
||||
When enabled, use the DNS servers listed below to resolve names outside the
|
||||
tailnet. When disabled (default), devices will prefer their local DNS
|
||||
configuration.
|
||||
<Link external styled to="https://tailscale.com/kb/1054/dns#global-nameservers">
|
||||
Learn More
|
||||
</Link>
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
<p>Override DNS servers</p>
|
||||
<Switch
|
||||
|
||||
@@ -61,16 +61,12 @@ export default function AddNameserver({ nameservers }: Props) {
|
||||
<div className="block">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Text className="font-semibold">Restrict to domain</Text>
|
||||
<Tooltip>
|
||||
<Tooltip content="Only clients that support split DNS (Tailscale v1.8 or later for most platforms) will use this nameserver. Older clients will ignore it.">
|
||||
<Chip
|
||||
className={cn("inline-flex items-center")}
|
||||
leftIcon={<Split className="mr-0.5 h-3 w-3" />}
|
||||
text="Split DNS"
|
||||
/>
|
||||
<Tooltip.Body>
|
||||
Only clients that support split DNS (Tailscale v1.8 or later for most platforms)
|
||||
will use this nameserver. Older clients will ignore it.
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Text className="text-sm">This nameserver will only be used for some domains.</Text>
|
||||
|
||||
@@ -113,9 +113,8 @@ export default function Page({
|
||||
<div className="border-r border-mist-100 p-2 pr-4 dark:border-mist-800">
|
||||
<span className="flex items-center gap-x-1 text-sm text-mist-600 dark:text-mist-300">
|
||||
Managed by
|
||||
<Tooltip>
|
||||
<Tooltip content="By default, a machine’s permissions match its creator’s.">
|
||||
<Info className="p-1" />
|
||||
<Tooltip.Body>By default, a machine’s permissions match its creator’s.</Tooltip.Body>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<div className="mt-1 flex items-center gap-x-2.5">
|
||||
@@ -154,11 +153,8 @@ export default function Page({
|
||||
<div>
|
||||
<span className="flex items-center gap-x-1 text-mist-600 dark:text-mist-300">
|
||||
Approved
|
||||
<Tooltip>
|
||||
<Tooltip content="Traffic to these routes are being routed through this machine.">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
<Tooltip.Body>
|
||||
Traffic to these routes are being routed through this machine.
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<div className="mt-1">
|
||||
@@ -183,12 +179,8 @@ export default function Page({
|
||||
<div>
|
||||
<span className="flex items-center gap-x-1 text-mist-600 dark:text-mist-300">
|
||||
Awaiting Approval
|
||||
<Tooltip>
|
||||
<Tooltip content="This machine is advertising these routes, but they must be approved before traffic will be routed to them.">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
<Tooltip.Body>
|
||||
This machine is advertising these routes, but they must be approved before traffic
|
||||
will be routed to them.
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<div className="mt-1">
|
||||
@@ -213,11 +205,8 @@ export default function Page({
|
||||
<div>
|
||||
<span className="flex items-center gap-x-1 text-mist-600 dark:text-mist-300">
|
||||
Exit Node
|
||||
<Tooltip>
|
||||
<Tooltip content="Whether this machine can act as an exit node for your tailnet.">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
<Tooltip.Body>
|
||||
Whether this machine can act as an exit node for your tailnet.
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<div className="mt-1">
|
||||
|
||||
@@ -271,16 +271,19 @@ export default function Page({ loaderData }: Route.ComponentProps) {
|
||||
))}
|
||||
</button>
|
||||
{loaderData.magic ? (
|
||||
<Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
<span className="font-normal">
|
||||
Since MagicDNS is enabled, you can access devices based on their name and
|
||||
also at{" "}
|
||||
<Code>
|
||||
[name].
|
||||
{loaderData.magic}
|
||||
</Code>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Info className="h-4 w-4" />
|
||||
<Tooltip.Body className="font-normal">
|
||||
Since MagicDNS is enabled, you can access devices based on their name and
|
||||
also at{" "}
|
||||
<Code>
|
||||
[name].
|
||||
{loaderData.magic}
|
||||
</Code>
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user