mirror of
https://github.com/tale/headplane.git
synced 2026-09-04 11:15:42 +00:00
feat: refactor several components and clean up ui
This commit is contained in:
@@ -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
@@ -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} />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user