mirror of
https://github.com/tale/headplane.git
synced 2026-08-30 16:59:56 +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}
|
{name}
|
||||||
{tooltip ? (
|
{tooltip ? (
|
||||||
<Tooltip>
|
<Tooltip content={tooltip}>
|
||||||
<Info className="size-4" />
|
<Info className="size-4" />
|
||||||
<Tooltip.Body>{tooltip}</Tooltip.Body>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</dt>
|
</dt>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function Code({ isCopyable, children, className }: CodeProps) {
|
|||||||
<button
|
<button
|
||||||
className="absolute right-0 bottom-0"
|
className="absolute right-0 bottom-0"
|
||||||
onClick={async (event) => {
|
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");
|
const svgs = event.currentTarget.querySelectorAll("svg");
|
||||||
for (const svg of svgs) {
|
for (const svg of svgs) {
|
||||||
|
|||||||
+55
-52
@@ -1,86 +1,89 @@
|
|||||||
import { useRef } from "react";
|
import { Tabs as BaseTabs } from "@base-ui/react/tabs";
|
||||||
import { AriaTabListProps, AriaTabPanelProps, useTab, useTabList, useTabPanel } from "react-aria";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { Item, Node, TabListState, useTabListState } from "react-stately";
|
|
||||||
|
|
||||||
import cn from "~/utils/cn";
|
import cn from "~/utils/cn";
|
||||||
|
|
||||||
export interface TabsProps extends AriaTabListProps<object> {
|
export interface TabsProps {
|
||||||
label: string;
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
defaultValue?: string | number;
|
||||||
|
value?: string | number;
|
||||||
|
onValueChange?: (value: string | number) => void;
|
||||||
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tabs({ label, className, ...props }: TabsProps) {
|
function Tabs({ label, className, children, ...props }: TabsProps) {
|
||||||
const state = useTabListState(props);
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const { tabListProps } = useTabList(props, state, ref);
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col", className)}>
|
<BaseTabs.Root
|
||||||
<div
|
{...props}
|
||||||
{...tabListProps}
|
defaultValue={props.defaultValue ?? 0}
|
||||||
className={cn(
|
aria-label={label}
|
||||||
"flex items-center rounded-t-lg w-fit max-w-full overflow-x-auto",
|
className={cn("flex flex-col", className)}
|
||||||
"border-mist-100 dark:border-mist-800",
|
>
|
||||||
"border-t border-x",
|
{children}
|
||||||
)}
|
</BaseTabs.Root>
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{[...state.collection].map((item) => (
|
|
||||||
<Tab item={item} key={item.key} state={state} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<TabsPanel key={state.selectedItem?.key} state={state} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsTabProps {
|
function TabList({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
item: Node<object>;
|
return (
|
||||||
state: TabListState<object>;
|
<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) {
|
function Tab({
|
||||||
const { key, rendered } = item;
|
value,
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
children,
|
||||||
|
className,
|
||||||
const { tabProps } = useTab({ key }, state, ref);
|
...props
|
||||||
|
}: ComponentProps<typeof BaseTabs.Tab> & { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<BaseTabs.Tab
|
||||||
{...tabProps}
|
value={value}
|
||||||
|
{...props}
|
||||||
className={cn(
|
className={cn(
|
||||||
"pl-2 pr-3 py-2.5",
|
"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",
|
"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",
|
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
|
||||||
"border-r border-mist-100 dark:border-mist-800",
|
"border-r border-mist-100 dark:border-mist-800",
|
||||||
"first:rounded-tl-lg last:rounded-tr-lg last:border-r-0",
|
"first:rounded-tl-lg last:rounded-tr-lg last:border-r-0",
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
>
|
>
|
||||||
{rendered}
|
{children}
|
||||||
</div>
|
</BaseTabs.Tab>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsPanelProps extends AriaTabPanelProps {
|
function Panel({
|
||||||
state: TabListState<object>;
|
value,
|
||||||
}
|
children,
|
||||||
|
className,
|
||||||
function TabsPanel({ state, ...props }: TabsPanelProps) {
|
...props
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
}: ComponentProps<typeof BaseTabs.Panel> & { className?: string }) {
|
||||||
const { tabPanelProps } = useTabPanel(props, state, ref);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<BaseTabs.Panel
|
||||||
{...tabPanelProps}
|
value={value}
|
||||||
|
{...props}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full overflow-clip rounded-b-lg rounded-r-lg",
|
"w-full overflow-clip rounded-b-lg rounded-r-lg",
|
||||||
"border border-mist-100 dark:border-mist-800",
|
"border border-mist-100 dark:border-mist-800",
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
>
|
>
|
||||||
{state.selectedItem?.props.children}
|
{children}
|
||||||
</div>
|
</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) {
|
export function ExitNodeTag({ isEnabled }: ExitNodeTagProps) {
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip
|
||||||
<Chip
|
content={
|
||||||
text="Exit Node"
|
isEnabled ? (
|
||||||
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 ? (
|
|
||||||
<>This machine is acting as an exit node.</>
|
<>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
|
This machine is requesting to be used as an exit node. Review this from the "Edit route
|
||||||
settings..." option in the machine's menu.
|
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>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,23 +14,24 @@ export function ExpiryTag({ variant, expiry }: ExpiryTagProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip
|
||||||
<Chip
|
content={
|
||||||
text={
|
variant === "expired" ? (
|
||||||
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" ? (
|
|
||||||
<>
|
<>
|
||||||
This machine is expired and will not be able to connect to the network. Re-authenticate
|
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.
|
with Tailscale on the machine to re-enable it.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>This machine has key expiry disabled and will never need to re-authenticate.</>
|
<>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>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,11 @@ import Tooltip from "../tooltip";
|
|||||||
|
|
||||||
export function HeadplaneAgentTag() {
|
export function HeadplaneAgentTag() {
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip content="This machine is running the Headplane agent, which allows it to provide host information in the web UI.">
|
||||||
<Chip
|
<Chip
|
||||||
text="Headplane Agent"
|
text="Headplane Agent"
|
||||||
className={cn("bg-purple-300 text-purple-900 dark:bg-purple-900 dark:text-purple-300")}
|
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>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,22 +11,23 @@ export interface SubnetTagProps {
|
|||||||
|
|
||||||
export function SubnetTag({ isEnabled }: SubnetTagProps) {
|
export function SubnetTag({ isEnabled }: SubnetTagProps) {
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip
|
||||||
<Chip
|
content={
|
||||||
text="Subnets"
|
isEnabled ? (
|
||||||
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 ? (
|
|
||||||
<>This machine advertises subnet routes.</>
|
<>This machine advertises subnet routes.</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
This machine has unadvertised subnet routes. Review this from the "Edit route
|
This machine has unadvertised subnet routes. Review this from the "Edit route
|
||||||
settings..." option in the machine's menu.
|
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>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,11 @@ import Tooltip from "../tooltip";
|
|||||||
|
|
||||||
export function TailscaleSSHTag() {
|
export function TailscaleSSHTag() {
|
||||||
return (
|
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
|
<Chip
|
||||||
text="Tailscale SSH"
|
text="Tailscale SSH"
|
||||||
className={cn("bg-lime-500 text-lime-900 dark:bg-lime-900 dark:text-lime-500")}
|
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>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-71
@@ -1,80 +1,45 @@
|
|||||||
import React, { cloneElement, useRef } from "react";
|
import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
|
||||||
import { AriaTooltipProps, mergeProps, useTooltip, useTooltipTrigger } from "react-aria";
|
import type { ReactNode } from "react";
|
||||||
import { TooltipTriggerState, useTooltipTriggerState } from "react-stately";
|
|
||||||
|
|
||||||
import cn from "~/utils/cn";
|
import cn from "~/utils/cn";
|
||||||
|
|
||||||
export interface TooltipProps extends AriaTooltipProps {
|
export interface TooltipProps {
|
||||||
children: [React.ReactElement, React.ReactElement<TooltipBodyProps>];
|
children: ReactNode;
|
||||||
}
|
content: ReactNode;
|
||||||
|
|
||||||
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;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Body({ state, className, ...props }: TooltipBodyProps) {
|
export default function Tooltip({ children, content, className }: TooltipProps) {
|
||||||
const { tooltipProps } = useTooltip(props, state);
|
|
||||||
return (
|
return (
|
||||||
<span
|
<BaseTooltip.Root>
|
||||||
{...mergeProps(props, tooltipProps)}
|
<BaseTooltip.Trigger
|
||||||
className={cn(
|
delay={0}
|
||||||
"absolute z-50 p-3 top-full mt-1",
|
closeDelay={0}
|
||||||
"outline-hidden rounded-lg text-sm w-48",
|
className={cn(
|
||||||
"bg-white dark:bg-mist-950",
|
"inline-flex items-center justify-center rounded-md",
|
||||||
"text-black dark:text-white",
|
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1",
|
||||||
"shadow-overlay",
|
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
|
||||||
"border border-mist-100 dark:border-mist-800",
|
)}
|
||||||
className,
|
>
|
||||||
)}
|
{children}
|
||||||
>
|
</BaseTooltip.Trigger>
|
||||||
{props.children}
|
<BaseTooltip.Portal>
|
||||||
</span>
|
<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 Link from "~/components/link";
|
||||||
import Notice from "~/components/notice";
|
import Notice from "~/components/notice";
|
||||||
import PageError from "~/components/page-error";
|
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 { isApiError } from "~/server/headscale/api/error-client";
|
||||||
import toast from "~/utils/toast";
|
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."}
|
"An unknown error occurred while trying to update the ACL policy."}
|
||||||
</Notice>
|
</Notice>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Tabs className="mb-4" label="ACL Editor">
|
<Tabs className="mb-4" label="ACL Editor" defaultValue="edit">
|
||||||
<Tabs.Item
|
<TabsList>
|
||||||
key="edit"
|
<TabsTab value="edit">
|
||||||
title={
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Pencil className="p-1" />
|
<Pencil className="p-1" />
|
||||||
<span>Edit file</span>
|
<span>Edit file</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
</TabsTab>
|
||||||
>
|
<TabsTab value="diff">
|
||||||
<Editor isDisabled={disabled} onChange={setCodePolicy} value={codePolicy} />
|
|
||||||
</Tabs.Item>
|
|
||||||
<Tabs.Item
|
|
||||||
key="diff"
|
|
||||||
title={
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Eye className="p-1" />
|
<Eye className="p-1" />
|
||||||
<span>Preview changes</span>
|
<span>Preview changes</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
</TabsTab>
|
||||||
>
|
<TabsTab value="preview">
|
||||||
<Differ left={policy} right={codePolicy} />
|
|
||||||
</Tabs.Item>
|
|
||||||
<Tabs.Item
|
|
||||||
key="preview"
|
|
||||||
title={
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FlaskConical className="p-1" />
|
<FlaskConical className="p-1" />
|
||||||
<span>Preview rules</span>
|
<span>Preview rules</span>
|
||||||
</div>
|
</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">
|
<div className="flex flex-col items-center py-8">
|
||||||
<Construction />
|
<Construction />
|
||||||
<p className="mt-4 w-1/2 text-center">
|
<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.
|
pretty complicated to implement. Hopefully I will be able to get to it soon.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Item>
|
</TabsPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Button
|
<Button
|
||||||
className="mr-2"
|
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">
|
<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>
|
<h2 className="text-md font-medium opacity-80">Global Nameservers</h2>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<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" />
|
<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>
|
</Tooltip>
|
||||||
<p>Override DNS servers</p>
|
<p>Override DNS servers</p>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -61,16 +61,12 @@ export default function AddNameserver({ nameservers }: Props) {
|
|||||||
<div className="block">
|
<div className="block">
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
<Text className="font-semibold">Restrict to domain</Text>
|
<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
|
<Chip
|
||||||
className={cn("inline-flex items-center")}
|
className={cn("inline-flex items-center")}
|
||||||
leftIcon={<Split className="mr-0.5 h-3 w-3" />}
|
leftIcon={<Split className="mr-0.5 h-3 w-3" />}
|
||||||
text="Split DNS"
|
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>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Text className="text-sm">This nameserver will only be used for some domains.</Text>
|
<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">
|
<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">
|
<span className="flex items-center gap-x-1 text-sm text-mist-600 dark:text-mist-300">
|
||||||
Managed by
|
Managed by
|
||||||
<Tooltip>
|
<Tooltip content="By default, a machine’s permissions match its creator’s.">
|
||||||
<Info className="p-1" />
|
<Info className="p-1" />
|
||||||
<Tooltip.Body>By default, a machine’s permissions match its creator’s.</Tooltip.Body>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-1 flex items-center gap-x-2.5">
|
<div className="mt-1 flex items-center gap-x-2.5">
|
||||||
@@ -154,11 +153,8 @@ export default function Page({
|
|||||||
<div>
|
<div>
|
||||||
<span className="flex items-center gap-x-1 text-mist-600 dark:text-mist-300">
|
<span className="flex items-center gap-x-1 text-mist-600 dark:text-mist-300">
|
||||||
Approved
|
Approved
|
||||||
<Tooltip>
|
<Tooltip content="Traffic to these routes are being routed through this machine.">
|
||||||
<Info className="h-3.5 w-3.5" />
|
<Info className="h-3.5 w-3.5" />
|
||||||
<Tooltip.Body>
|
|
||||||
Traffic to these routes are being routed through this machine.
|
|
||||||
</Tooltip.Body>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -183,12 +179,8 @@ export default function Page({
|
|||||||
<div>
|
<div>
|
||||||
<span className="flex items-center gap-x-1 text-mist-600 dark:text-mist-300">
|
<span className="flex items-center gap-x-1 text-mist-600 dark:text-mist-300">
|
||||||
Awaiting Approval
|
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" />
|
<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>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -213,11 +205,8 @@ export default function Page({
|
|||||||
<div>
|
<div>
|
||||||
<span className="flex items-center gap-x-1 text-mist-600 dark:text-mist-300">
|
<span className="flex items-center gap-x-1 text-mist-600 dark:text-mist-300">
|
||||||
Exit Node
|
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" />
|
<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>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
|
|||||||
@@ -271,16 +271,19 @@ export default function Page({ loaderData }: Route.ComponentProps) {
|
|||||||
))}
|
))}
|
||||||
</button>
|
</button>
|
||||||
{loaderData.magic ? (
|
{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" />
|
<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>
|
</Tooltip>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user