chore: combine link usage

This commit is contained in:
Aarnav Tale
2026-03-09 13:56:03 -04:00
parent 37d0080cba
commit a53fd31d27
18 changed files with 114 additions and 73 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
import { CircleX } from "lucide-react"; import { CircleX } from "lucide-react";
import Link from "~/components/Link"; import Link from "~/components/link";
import cn from "~/utils/cn"; import cn from "~/utils/cn";
interface FooterProps { interface FooterProps {
@@ -28,7 +28,7 @@ export default function Footer({ url, debug, healthy }: FooterProps) {
<div className={cn("text-xs leading-none", !healthy && "hidden md:block")}> <div className={cn("text-xs leading-none", !healthy && "hidden md:block")}>
<p> <p>
Headplane is free. Please consider{" "} Headplane is free. Please consider{" "}
<Link to="https://github.com/sponsors/tale" name="Aarnav's GitHub Sponsors"> <Link isExternal to="https://github.com/sponsors/tale" name="Aarnav's GitHub Sponsors">
donating donating
</Link>{" "} </Link>{" "}
to support development.{" "} to support development.{" "}
-32
View File
@@ -1,32 +0,0 @@
import { ExternalLink } from "lucide-react";
import cn from "~/utils/cn";
export interface LinkProps {
to: string;
name: string;
children: string;
className?: string;
}
export default function Link({ to, name: alt, children, className }: LinkProps) {
return (
<a
href={to}
aria-label={alt}
target="_blank"
rel="noreferrer"
className={cn(
"inline-flex items-center gap-x-0.5",
"text-blue-500 hover:text-blue-700",
"dark:text-blue-400 dark:hover:text-blue-300",
"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",
className,
)}
>
{children}
<ExternalLink className="w-3.5" />
</a>
);
}
+2 -2
View File
@@ -6,7 +6,7 @@ import cn from "~/utils/cn";
import Card from "./Card"; import Card from "./Card";
import Code from "./Code"; import Code from "./Code";
import Link from "./Link"; import Link from "./link";
export function getErrorMessage(error: Error | unknown): { export function getErrorMessage(error: Error | unknown): {
title: string; title: string;
@@ -137,7 +137,7 @@ export function getErrorMessage(error: Error | unknown): {
<Card.Text> <Card.Text>
An unexpected error occurred which is most likely a bug. Please consider reporting An unexpected error occurred which is most likely a bug. Please consider reporting
filing an issue on the{" "} filing an issue on the{" "}
<Link name="Headplane GitHub" to="https://github.com/tale/headplane/issues"> <Link isExternal name="Headplane GitHub" to="https://github.com/tale/headplane/issues">
Headplane GitHub Headplane GitHub
</Link>{" "} </Link>{" "}
repository with the details below. repository with the details below.
+52
View File
@@ -0,0 +1,52 @@
import { ExternalLink } from "lucide-react";
import type { JSX } from "react";
import { Link as RouterLink } from "react-router";
import cn from "~/utils/cn";
const linkStyles = cn(
"inline-flex items-center gap-x-0.5",
"text-blue-500 hover:text-blue-700",
"dark:text-blue-400 dark:hover:text-blue-300",
"focus:ring-offset-1 rounded-md",
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
);
export type LinkProps =
| {
isExternal: true;
to: string;
name: string;
children: string;
className?: string;
}
| {
isExternal?: false;
to: string;
children?: string;
className?: string;
};
export default function Link(props: LinkProps): JSX.Element {
if (props.isExternal) {
return (
<a
href={props.to}
aria-label={props.name}
target="_blank"
rel="noreferrer"
className={cn(linkStyles, props.className)}
>
{props.children}
<ExternalLink className="w-3.5" />
</a>
);
}
return (
<RouterLink to={props.to} className={cn(linkStyles, props.className)}>
{props.children}
</RouterLink>
);
}
+3 -1
View File
@@ -3,7 +3,7 @@ import { Form } from "react-router";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Card from "~/components/Card"; import Card from "~/components/Card";
import Link from "~/components/Link"; import Link from "~/components/link";
import Options from "~/components/Options"; import Options from "~/components/Options";
import toast from "~/utils/toast"; import toast from "~/utils/toast";
@@ -58,6 +58,7 @@ export default function NoAccess({ linkedUserName, osValue }: NoAccessProps) {
<p className="mt-1 text-center text-xs text-mist-600 dark:text-mist-300"> <p className="mt-1 text-center text-xs text-mist-600 dark:text-mist-300">
Click this button to copy the command.{" "} Click this button to copy the command.{" "}
<Link <Link
isExternal
name="Linux installation script" name="Linux installation script"
to="https://github.com/tailscale/tailscale/blob/main/scripts/installer.sh" to="https://github.com/tailscale/tailscale/blob/main/scripts/installer.sh"
> >
@@ -112,6 +113,7 @@ export default function NoAccess({ linkedUserName, osValue }: NoAccessProps) {
<br /> <br />
You can also download Tailscale on the{" "} You can also download Tailscale on the{" "}
<Link <Link
isExternal
name="macOS App Store" name="macOS App Store"
to="https://apps.apple.com/ca/app/tailscale/id1475387142" to="https://apps.apple.com/ca/app/tailscale/id1475387142"
> >
+2 -1
View File
@@ -2,7 +2,7 @@ import { AlertCircle, CloudOff } from "lucide-react";
import Card from "~/components/Card"; import Card from "~/components/Card";
import Code from "~/components/Code"; import Code from "~/components/Code";
import Link from "~/components/Link"; import Link from "~/components/link";
import { OidcConnectorError } from "~/server/web/oidc-connector"; import { OidcConnectorError } from "~/server/web/oidc-connector";
export function OidcDiscoveryFailedNotice() { export function OidcDiscoveryFailedNotice() {
@@ -35,6 +35,7 @@ export function OidcConfigErrorNotice({ errors }: { errors: OidcConnectorError[]
))} ))}
</ul>{" "} </ul>{" "}
<Link <Link
isExternal
name="Headplane OIDC Issues" name="Headplane OIDC Issues"
to="https://headplane.net/configuration/sso#troubleshooting" to="https://headplane.net/configuration/sso#troubleshooting"
> >
+5 -4
View File
@@ -1,12 +1,12 @@
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Form, Link as RemixLink, redirect, useSearchParams } from "react-router"; import { Form, redirect, useSearchParams } from "react-router";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Card from "~/components/Card"; import Card from "~/components/Card";
import Code from "~/components/Code"; import Code from "~/components/Code";
import Input from "~/components/Input"; import Input from "~/components/Input";
import Link from "~/components/Link"; import Link from "~/components/link";
import { useLiveData } from "~/utils/live-data"; import { useLiveData } from "~/utils/live-data";
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
@@ -103,6 +103,7 @@ export default function Page({ loaderData, actionData }: Route.ComponentProps) {
Headplane is configured to use secure cookies, but this site is being served over an Headplane is configured to use secure cookies, but this site is being served over an
insecure connection and login will not work correctly.{" "} insecure connection and login will not work correctly.{" "}
<Link <Link
isExternal
name="Headplane Common Issues" name="Headplane Common Issues"
to="https://headplane.net/configuration/common-issues#issue-logging-in-does-not-do-anything" to="https://headplane.net/configuration/common-issues#issue-logging-in-does-not-do-anything"
> >
@@ -138,7 +139,7 @@ export default function Page({ loaderData, actionData }: Route.ComponentProps) {
</Button> </Button>
</Form> </Form>
{isOidcConnectorEnabled ? ( {isOidcConnectorEnabled ? (
<RemixLink to="/oidc/start"> <Link to="/oidc/start">
<Button <Button
className="mt-2 w-full" className="mt-2 w-full"
isDisabled={oidcErrorCodes.length > 0} isDisabled={oidcErrorCodes.length > 0}
@@ -146,7 +147,7 @@ export default function Page({ loaderData, actionData }: Route.ComponentProps) {
> >
Single Sign-On Single Sign-On
</Button> </Button>
</RemixLink> </Link>
) : undefined} ) : undefined}
</Card> </Card>
</div> </div>
+6 -2
View File
@@ -2,7 +2,7 @@ import { Form } from "react-router";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Code from "~/components/Code"; import Code from "~/components/Code";
import Link from "~/components/Link"; import Link from "~/components/link";
import TableList from "~/components/TableList"; import TableList from "~/components/TableList";
import cn from "~/utils/cn"; import cn from "~/utils/cn";
@@ -20,7 +20,11 @@ export default function ManageRecords({ records, isDisabled }: Props) {
<p> <p>
Headscale supports adding custom DNS records to your Tailnet. As of now, only <Code>A</Code>{" "} Headscale supports adding custom DNS records to your Tailnet. As of now, only <Code>A</Code>{" "}
and <Code>AAAA</Code> records are supported.{" "} and <Code>AAAA</Code> records are supported.{" "}
<Link name="Headscale DNS Records documentation" to="https://headscale.net/stable/ref/dns"> <Link
isExternal
name="Headscale DNS Records documentation"
to="https://headscale.net/stable/ref/dns"
>
Learn More Learn More
</Link> </Link>
</p> </p>
@@ -1,8 +1,8 @@
import { ChevronDown, Copy } from "lucide-react"; import { ChevronDown, Copy } from "lucide-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { Link } from "react-router";
import Chip from "~/components/Chip"; import Chip from "~/components/Chip";
import Link from "~/components/link";
import Menu from "~/components/Menu"; import Menu from "~/components/Menu";
import StatusCircle from "~/components/StatusCircle"; import StatusCircle from "~/components/StatusCircle";
import { ExitNodeTag } from "~/components/tags/ExitNode"; import { ExitNodeTag } from "~/components/tags/ExitNode";
+7 -4
View File
@@ -2,13 +2,12 @@ import { Plus, TagsIcon, X } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useFetcher } from "react-router"; import { useFetcher } from "react-router";
import type { Machine } from "~/types";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Dialog from "~/components/Dialog"; import Dialog from "~/components/Dialog";
import Link from "~/components/Link"; import Link from "~/components/link";
import Select from "~/components/Select"; import Select from "~/components/Select";
import TableList from "~/components/TableList"; import TableList from "~/components/TableList";
import type { Machine } from "~/types";
import cn from "~/utils/cn"; import cn from "~/utils/cn";
interface TagsProps { interface TagsProps {
@@ -73,7 +72,11 @@ export default function Tags({ machine, isOpen, setIsOpen, existingTags }: TagsP
<Dialog.Title>Edit ACL tags for {machine.givenName}</Dialog.Title> <Dialog.Title>Edit ACL tags for {machine.givenName}</Dialog.Title>
<Dialog.Text> <Dialog.Text>
ACL tags can be used to reference machines in your ACL policies. See the{" "} ACL tags can be used to reference machines in your ACL policies. See the{" "}
<Link name="Tailscale documentation" to="https://tailscale.com/kb/1068/acl-tags"> <Link
isExternal
name="Tailscale documentation"
to="https://tailscale.com/kb/1068/acl-tags"
>
Tailscale documentation Tailscale documentation
</Link>{" "} </Link>{" "}
for more information. for more information.
+9 -5
View File
@@ -1,12 +1,12 @@
import { CheckCircle, CircleSlash, Info, UserCircle } from "lucide-react"; import { CheckCircle, CircleSlash, Info, UserCircle } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { data, Link as RemixLink } from "react-router"; import { data } from "react-router";
import Attribute from "~/components/Attribute"; import Attribute from "~/components/Attribute";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Card from "~/components/Card"; import Card from "~/components/Card";
import Chip from "~/components/Chip"; import Chip from "~/components/Chip";
import Link from "~/components/Link"; import Link from "~/components/link";
import StatusCircle from "~/components/StatusCircle"; import StatusCircle from "~/components/StatusCircle";
import Tooltip from "~/components/Tooltip"; import Tooltip from "~/components/Tooltip";
import cn from "~/utils/cn"; import cn from "~/utils/cn";
@@ -75,9 +75,9 @@ export default function Page({
return ( return (
<div> <div>
<p className="text-md mb-8"> <p className="text-md mb-8">
<RemixLink className="font-medium" to="/machines"> <Link className="font-medium" to="/machines">
All Machines All Machines
</RemixLink> </Link>
<span className="mx-2">/</span> <span className="mx-2">/</span>
{node.givenName} {node.givenName}
</p> </p>
@@ -129,7 +129,11 @@ export default function Page({
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<p> <p>
Subnets let you expose physical network routes onto Tailscale.{" "} Subnets let you expose physical network routes onto Tailscale.{" "}
<Link name="Tailscale Subnets Documentation" to="https://tailscale.com/kb/1019/subnets"> <Link
isExternal
name="Tailscale Subnets Documentation"
to="https://tailscale.com/kb/1019/subnets"
>
Learn More Learn More
</Link> </Link>
</p> </p>
+2 -1
View File
@@ -3,7 +3,7 @@ import { useMemo, useState } from "react";
import Code from "~/components/Code"; import Code from "~/components/Code";
import Input from "~/components/Input"; import Input from "~/components/Input";
import Link from "~/components/Link"; import Link from "~/components/link";
import Tooltip from "~/components/Tooltip"; import Tooltip from "~/components/Tooltip";
import { Capabilities } from "~/server/web/roles"; import { Capabilities } from "~/server/web/roles";
import cn from "~/utils/cn"; import cn from "~/utils/cn";
@@ -151,6 +151,7 @@ export default function Page({ loaderData }: Route.ComponentProps) {
<p> <p>
Manage the devices connected to your Tailnet.{" "} Manage the devices connected to your Tailnet.{" "}
<Link <Link
isExternal
name="Tailscale Manage Devices Documentation" name="Tailscale Manage Devices Documentation"
to="https://tailscale.com/kb/1372/manage-devices" to="https://tailscale.com/kb/1372/manage-devices"
> >
@@ -5,7 +5,7 @@ import Button from "~/components/Button";
import Code from "~/components/Code"; import Code from "~/components/Code";
import Dialog from "~/components/Dialog"; import Dialog from "~/components/Dialog";
import Input from "~/components/Input"; import Input from "~/components/Input";
import Link from "~/components/Link"; import Link from "~/components/link";
import NumberInput from "~/components/NumberInput"; import NumberInput from "~/components/NumberInput";
import Select from "~/components/Select"; import Select from "~/components/Select";
import Switch from "~/components/Switch"; import Switch from "~/components/Switch";
@@ -204,6 +204,7 @@ export default function AddAuthKey({
Devices authenticated with this key will be automatically removed once they go Devices authenticated with this key will be automatically removed once they go
offline.{" "} offline.{" "}
<Link <Link
isExternal
name="Tailscale Ephemeral Nodes Documentation" name="Tailscale Ephemeral Nodes Documentation"
to="https://tailscale.com/kb/1111/ephemeral-nodes" to="https://tailscale.com/kb/1111/ephemeral-nodes"
> >
+4 -4
View File
@@ -1,9 +1,8 @@
import { FileKey2 } from "lucide-react"; import { FileKey2 } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { Link as RemixLink } from "react-router";
import Code from "~/components/Code"; 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 Select from "~/components/Select"; import Select from "~/components/Select";
import TableList from "~/components/TableList"; import TableList from "~/components/TableList";
@@ -167,9 +166,9 @@ export default function Page({
return ( return (
<div className="flex flex-col md:w-2/3"> <div className="flex flex-col md:w-2/3">
<p className="text-md mb-8"> <p className="text-md mb-8">
<RemixLink className="font-medium" to="/settings"> <Link className="font-medium" to="/settings">
Settings Settings
</RemixLink> </Link>
<span className="mx-2">/</span> Pre-Auth Keys <span className="mx-2">/</span> Pre-Auth Keys
</p> </p>
{!access ? ( {!access ? (
@@ -194,6 +193,7 @@ export default function Page({
Headscale fully supports pre-authentication keys in order to easily add devices to your 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{" "} Tailnet. To learn more about using pre-authentication keys, visit the{" "}
<Link <Link
isExternal
name="Tailscale Auth Keys documentation" name="Tailscale Auth Keys documentation"
to="https://tailscale.com/kb/1085/auth-keys/" to="https://tailscale.com/kb/1085/auth-keys/"
> >
+7 -6
View File
@@ -1,7 +1,6 @@
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import { Link as RemixLink } from "react-router";
import Link from "~/components/Link"; import Link from "~/components/link";
import type { Route } from "./+types/overview"; import type { Route } from "./+types/overview";
@@ -30,6 +29,7 @@ export default function Page({ loaderData: { config, isOidcEnabled } }: Route.Co
Headscale fully supports pre-authentication keys in order to easily add devices to your 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{" "} Tailnet. To learn more about using pre-authentication keys, visit the{" "}
<Link <Link
isExternal
name="Tailscale Auth Keys documentation" name="Tailscale Auth Keys documentation"
to="https://tailscale.com/kb/1085/auth-keys/" to="https://tailscale.com/kb/1085/auth-keys/"
> >
@@ -37,12 +37,12 @@ export default function Page({ loaderData: { config, isOidcEnabled } }: Route.Co
</Link> </Link>
</p> </p>
</div> </div>
<RemixLink to="/settings/auth-keys"> <Link to="/settings/auth-keys">
<div className="flex items-center text-lg font-medium"> <div className="flex items-center text-lg font-medium">
Manage Auth Keys Manage Auth Keys
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
</div> </div>
</RemixLink> </Link>
{config && isOidcEnabled ? ( {config && isOidcEnabled ? (
<> <>
<div className="flex w-full flex-col sm:w-2/3"> <div className="flex w-full flex-col sm:w-2/3">
@@ -53,6 +53,7 @@ export default function Page({ loaderData: { config, isOidcEnabled } }: Route.Co
Tailnet to only certain users or groups and Headplane will also respect these settings Tailnet to only certain users or groups and Headplane will also respect these settings
when authenticating.{" "} when authenticating.{" "}
<Link <Link
isExternal
name="Headscale OIDC documentation" name="Headscale OIDC documentation"
to="https://headscale.net/stable/ref/oidc/#basic-configuration" to="https://headscale.net/stable/ref/oidc/#basic-configuration"
> >
@@ -60,12 +61,12 @@ export default function Page({ loaderData: { config, isOidcEnabled } }: Route.Co
</Link> </Link>
</p> </p>
</div> </div>
<RemixLink to="/settings/restrictions"> <Link to="/settings/restrictions">
<div className="flex items-center text-lg font-medium"> <div className="flex items-center text-lg font-medium">
Manage Restrictions Manage Restrictions
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
</div> </div>
</RemixLink> </Link>
</> </>
) : undefined} ) : undefined}
</div> </div>
@@ -1,6 +1,6 @@
import { data, Link as RemixLink } from "react-router"; import { data } from "react-router";
import Link from "~/components/Link"; import Link from "~/components/link";
import Notice from "~/components/Notice"; import Notice from "~/components/Notice";
import { Capabilities } from "~/server/web/roles"; import { Capabilities } from "~/server/web/roles";
@@ -46,9 +46,9 @@ export default function Page({ loaderData: { access, writable, settings } }: Rou
<div className="flex max-w-(--breakpoint-lg) flex-col gap-4"> <div className="flex max-w-(--breakpoint-lg) flex-col gap-4">
<div className="flex w-full flex-col sm:w-2/3"> <div className="flex w-full flex-col sm:w-2/3">
<p className="text-md mb-4"> <p className="text-md mb-4">
<RemixLink className="font-medium" to="/settings"> <Link className="font-medium" to="/settings">
Settings Settings
</RemixLink> </Link>
<span className="mx-2">/</span> Authentication Restrictions <span className="mx-2">/</span> Authentication Restrictions
</p> </p>
{!access ? ( {!access ? (
@@ -70,6 +70,7 @@ export default function Page({ loaderData: { access, writable, settings } }: Rou
certain users or groups and Headplane will also respect these settings when certain users or groups and Headplane will also respect these settings when
authenticating.{" "} authenticating.{" "}
<Link <Link
isExternal
name="Headscale OIDC documentation" name="Headscale OIDC documentation"
to="https://headscale.net/stable/ref/oidc/#basic-configuration" to="https://headscale.net/stable/ref/oidc/#basic-configuration"
> >
@@ -1,7 +1,7 @@
import { Building2, House, Key } from "lucide-react"; import { Building2, House, Key } from "lucide-react";
import Card from "~/components/Card"; import Card from "~/components/Card";
import Link from "~/components/Link"; import Link from "~/components/link";
import CreateUser from "../dialogs/create-user"; import CreateUser from "../dialogs/create-user";
@@ -21,7 +21,7 @@ export default function ManageBanner({ oidc, isDisabled }: ManageBannerProps) {
{oidc ? ( {oidc ? (
<> <>
Users are managed through your{" "} Users are managed through your{" "}
<Link name="OIDC Provider" to={oidc.issuer}> <Link isExternal name="OIDC Provider" to={oidc.issuer}>
OpenID Connect provider OpenID Connect provider
</Link> </Link>
{". "} {". "}
+3 -1
View File
@@ -5,7 +5,7 @@ import { Form, NavLink } from "react-router";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Card from "~/components/Card"; import Card from "~/components/Card";
import Link from "~/components/Link"; import Link from "~/components/link";
import Options from "~/components/Options"; import Options from "~/components/Options";
import StatusCircle from "~/components/StatusCircle"; import StatusCircle from "~/components/StatusCircle";
import { findHeadscaleUserBySubject } from "~/server/web/headscale-identity"; import { findHeadscaleUserBySubject } from "~/server/web/headscale-identity";
@@ -216,6 +216,7 @@ export default function Page({
<p className="mt-1 text-center text-xs text-mist-600 dark:text-mist-300"> <p className="mt-1 text-center text-xs text-mist-600 dark:text-mist-300">
Click this button to copy the command.{" "} Click this button to copy the command.{" "}
<Link <Link
isExternal
name="Linux installation script" name="Linux installation script"
to="https://github.com/tailscale/tailscale/blob/main/scripts/installer.sh" to="https://github.com/tailscale/tailscale/blob/main/scripts/installer.sh"
> >
@@ -270,6 +271,7 @@ export default function Page({
<br /> <br />
You can also download Tailscale on the{" "} You can also download Tailscale on the{" "}
<Link <Link
isExternal
name="macOS App Store" name="macOS App Store"
to="https://apps.apple.com/ca/app/tailscale/id1475387142" to="https://apps.apple.com/ca/app/tailscale/id1475387142"
> >