feat: refactor several components and clean up ui

This commit is contained in:
Aarnav Tale
2026-03-09 22:09:58 -04:00
parent a53fd31d27
commit 6470f5a821
49 changed files with 1522 additions and 1355 deletions
-188
View File
@@ -1,188 +0,0 @@
import { Icon } from "@iconify/react";
import { Form } from "react-router";
import Button from "~/components/Button";
import Card from "~/components/Card";
import Link from "~/components/link";
import Options from "~/components/Options";
import toast from "~/utils/toast";
interface NoAccessProps {
linkedUserName?: string;
osValue?: string;
}
export default function NoAccess({ linkedUserName, osValue }: NoAccessProps) {
return (
<main className="container mt-6 mb-24 overscroll-contain">
<div className="mx-auto flex max-w-xl flex-col gap-4">
{linkedUserName ? (
<Card className="max-w-xl" variant="flat">
<p className="text-sm">
Your account is linked to Headscale user <strong>{linkedUserName}</strong>.
</p>
</Card>
) : undefined}
<Card className="max-w-xl" variant="flat">
<Card.Title className="mb-3">Access your network via Tailscale</Card.Title>
<Card.Text>
You don't have dashboard access, but you can still connect to your Headscale network.
Install Tailscale on your device to get started.
</Card.Text>
<Options
className="my-4"
defaultSelectedKey={osValue ?? "linux"}
label="Download Selector"
>
<Options.Item
key="linux"
title={
<div className="flex items-center gap-1">
<Icon className="ml-1 w-4" icon="ion:terminal" />
<span>Linux</span>
</div>
}
>
<Button
className="text-md flex font-mono"
onPress={async () => {
await navigator.clipboard.writeText(
"curl -fsSL https://tailscale.com/install.sh | sh",
);
toast("Copied to clipboard");
}}
>
curl -fsSL https://tailscale.com/install.sh | sh
</Button>
<p className="mt-1 text-center text-xs text-mist-600 dark:text-mist-300">
Click this button to copy the command.{" "}
<Link
isExternal
name="Linux installation script"
to="https://github.com/tailscale/tailscale/blob/main/scripts/installer.sh"
>
View script source
</Link>
</p>
</Options.Item>
<Options.Item
key="windows"
title={
<div className="flex items-center gap-1">
<Icon className="ml-1 w-4" icon="mdi:microsoft" />
<span>Windows</span>
</div>
}
>
<a
aria-label="Download for Windows"
href="https://pkgs.tailscale.com/stable/tailscale-setup-latest.exe"
rel="noreferrer"
target="_blank"
>
<Button className="my-4 w-full" variant="heavy">
Download for Windows
</Button>
</a>
<p className="text-center text-sm text-mist-600 dark:text-mist-300">
Requires Windows 10 or later.
</p>
</Options.Item>
<Options.Item
key="macos"
title={
<div className="flex items-center gap-1">
<Icon className="ml-1 w-4" icon="streamline-logos:mac-finder-logo-solid" />
<span>macOS</span>
</div>
}
>
<a
aria-label="Download for macOS"
href="https://pkgs.tailscale.com/stable/Tailscale-latest-macos.pkg"
rel="noreferrer"
target="_blank"
>
<Button className="my-4 w-full" variant="heavy">
Download for macOS
</Button>
</a>
<p className="text-center text-sm text-mist-600 dark:text-mist-300">
Requires macOS Big Sur 11.0 or later.
<br />
You can also download Tailscale on the{" "}
<Link
isExternal
name="macOS App Store"
to="https://apps.apple.com/ca/app/tailscale/id1475387142"
>
macOS App Store
</Link>
{"."}
</p>
</Options.Item>
<Options.Item
key="ios"
title={
<div className="flex items-center gap-1">
<Icon className="ml-1 w-4" icon="grommet-icons:apple" />
<span>iOS</span>
</div>
}
>
<a
aria-label="Download for iOS"
href="https://apps.apple.com/us/app/tailscale/id1470499037"
rel="noreferrer"
target="_blank"
>
<Button className="my-4 w-full" variant="heavy">
Download for iOS
</Button>
</a>
<p className="text-center text-sm text-mist-600 dark:text-mist-300">
Requires iOS 15 or later.
</p>
</Options.Item>
<Options.Item
key="android"
title={
<div className="flex items-center gap-1">
<Icon className="ml-1 w-4" icon="material-symbols:android" />
<span>Android</span>
</div>
}
>
<a
aria-label="Download for Android"
href="https://play.google.com/store/apps/details?id=com.tailscale.ipn"
rel="noreferrer"
target="_blank"
>
<Button className="my-4 w-full" variant="heavy">
Download for Android
</Button>
</a>
<p className="text-center text-sm text-mist-600 dark:text-mist-300">
Requires Android 8 or later.
</p>
</Options.Item>
</Options>
</Card>
<Card className="max-w-xl" variant="flat">
<Card.Title className="mb-3">Need dashboard access?</Card.Title>
<Card.Text>
Your account is signed in but doesn't have permission to manage the dashboard. Contact
an administrator to request access.
</Card.Text>
<Form action="/logout" className="mt-4" method="POST">
<Button className="w-full" type="submit" variant="light">
Sign out
</Button>
</Form>
</Card>
</div>
</main>
);
}
+27 -22
View File
@@ -1,11 +1,11 @@
import { Outlet, redirect } from "react-router";
import Footer from "~/components/Footer";
import Header from "~/components/Header";
import Header from "~/layout/header";
import { Capabilities } from "~/server/web/roles";
import { getUserDisplayName } from "~/utils/user";
import { Route } from "./+types/shell";
import type { Route } from "./+types/shell";
import NoAccess from "./no-access";
// This loads the bare minimum for the application to function
@@ -31,13 +31,13 @@ export async function loader({ request, context }: Route.LoaderArgs) {
const user =
principal.kind === "oidc"
? {
subject: principal.user.subject,
name: principal.profile.name,
email: principal.profile.email,
username: principal.profile.username,
name: principal.profile.name,
picture: principal.profile.picture,
subject: principal.user.subject,
username: principal.profile.username,
}
: { subject: "api_key", name: principal.displayName };
: { name: principal.displayName, subject: "api_key" };
let linkedUserName: string | undefined;
let osValue: string | undefined;
@@ -57,31 +57,31 @@ export async function loader({ request, context }: Route.LoaderArgs) {
const userAgent = request.headers.get("user-agent");
const os = userAgent?.match(/(Linux|Windows|Mac OS X|iPhone|iPad|Android)/);
switch (os?.[0]) {
case "Windows":
case "Windows": {
osValue = "windows";
break;
case "Mac OS X":
}
case "Mac OS X": {
osValue = "macos";
break;
}
case "iPhone":
case "iPad":
case "iPad": {
osValue = "ios";
break;
case "Android":
}
case "Android": {
osValue = "android";
break;
default:
}
default: {
osValue = "linux";
break;
}
}
}
return {
config: context.hs.c,
url: context.config.headscale.public_url ?? context.config.headscale.url,
configAvailable: context.hs.readable(),
debug: context.config.debug,
user,
access: {
ui: check,
dns: context.auth.can(principal, Capabilities.read_network),
@@ -90,11 +90,16 @@ export async function loader({ request, context }: Route.LoaderArgs) {
machines: context.auth.can(principal, Capabilities.read_machines),
settings: context.auth.can(principal, Capabilities.read_feature),
},
onboarding: request.url.endsWith("/onboarding"),
noAccess,
linkedUserName,
osValue,
config: context.hs.c,
configAvailable: context.hs.readable(),
debug: context.config.debug,
healthy: await api.isHealthy(),
linkedUserName,
noAccess,
onboarding: request.url.endsWith("/onboarding"),
osValue,
url: context.config.headscale.public_url ?? context.config.headscale.url,
user,
};
} catch {
return redirect("/login", {
@@ -109,7 +114,7 @@ export default function Shell({ loaderData }: Route.ComponentProps) {
if (loaderData.noAccess && !loaderData.onboarding) {
return (
<>
<Header {...loaderData} />
<Header user={loaderData.user} />
<NoAccess linkedUserName={loaderData.linkedUserName} osValue={loaderData.osValue} />
<Footer {...loaderData} />
</>
@@ -118,7 +123,7 @@ export default function Shell({ loaderData }: Route.ComponentProps) {
return (
<>
<Header {...loaderData} />
<Header user={loaderData.user} />
<Outlet />
<Footer {...loaderData} />
</>