mirror of
https://github.com/tale/headplane.git
synced 2026-08-19 17:36:19 +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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user