From 42dc843a87d49055e05cc0c51d91b22d45b9dc26 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Fri, 17 Nov 2023 16:05:14 -0800 Subject: [PATCH] client/web: add advanced login options This adds an expandable section of the login view to allow users to specify an auth key and an alternate control URL. Input and Collapsible components and accompanying styles were brought over from the adminpanel. Updates #10261 Signed-off-by: Will Norris --- client/web/package.json | 1 + .../web/src/components/views/login-view.tsx | 44 ++++++++++++++++++- client/web/src/index.css | 44 ++++++++++++++++++- client/web/src/ui/collapsible.tsx | 33 ++++++++++++++ client/web/src/ui/input.tsx | 41 +++++++++++++++++ client/web/tailwind.config.js | 21 ++++++--- client/web/web.go | 18 +++++++- client/web/yarn.lock | 15 +++++++ 8 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 client/web/src/ui/collapsible.tsx create mode 100644 client/web/src/ui/input.tsx diff --git a/client/web/package.json b/client/web/package.json index a986265d8..a0f70c87c 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -9,6 +9,7 @@ "private": true, "dependencies": { "@radix-ui/react-popover": "^1.0.6", + "@radix-ui/react-collapsible": "^1.0.3", "classnames": "^2.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/client/web/src/components/views/login-view.tsx b/client/web/src/components/views/login-view.tsx index 2adeaeab9..498ff6c61 100644 --- a/client/web/src/components/views/login-view.tsx +++ b/client/web/src/components/views/login-view.tsx @@ -1,7 +1,9 @@ -import React, { useCallback } from "react" +import React, { useCallback, useState } from "react" import { apiFetch } from "src/api" import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg" import { NodeData } from "src/hooks/node-data" +import Collapsible from "src/ui/collapsible" +import Input from "src/ui/input" /** * LoginView is rendered when the client is not authenticated @@ -14,6 +16,9 @@ export default function LoginView({ data: NodeData refreshData: () => void }) { + const [controlURL, setControlURL] = useState("") + const [authKey, setAuthKey] = useState("") + const login = useCallback( (opt: TailscaleUpOptions) => { tailscaleUp(opt).then(refreshData) @@ -76,11 +81,44 @@ export default function LoginView({

+ +

Auth Key

+

+ Connect with a pre-authenticated key.{" "} + + Learn more → + +

+ setAuthKey(e.target.value)} + placeholder="tskey-auth-XXX" + /> +

Server URL

+

Base URL of control server.

+ setControlURL(e.target.value)} + placeholder="https://login.tailscale.com/" + /> +
)} @@ -89,6 +127,8 @@ export default function LoginView({ type TailscaleUpOptions = { Reauthenticate?: boolean // force reauthentication + ControlURL?: string + AuthKey?: string } function tailscaleUp(options: TailscaleUpOptions) { diff --git a/client/web/src/index.css b/client/web/src/index.css index 5a2fca28b..9f6bedab9 100644 --- a/client/web/src/index.css +++ b/client/web/src/index.css @@ -45,7 +45,7 @@ } .description { - @apply text-neutral-500 leading-snug + @apply text-neutral-500 leading-snug; } /** @@ -144,6 +144,48 @@ .toggle-small:checked:enabled:active::after { @apply w-[0.675rem] translate-x-[0.55rem]; } + + /** + * .input defines default text input field styling. These styles should + * correspond to .button, sharing a similar height and rounding, since .input + * and .button are commonly used together. + */ + + .input, + .input-wrapper { + @apply appearance-none leading-tight rounded-md bg-white border border-gray-300 hover:border-gray-400 transition-colors w-full h-input; + } + + .input { + @apply px-3; + } + + .input::placeholder, + .input-wrapper::placeholder { + @apply text-gray-400; + } + + .input:disabled, + .input-wrapper:disabled { + @apply border-gray-300; + @apply bg-gray-0; + @apply cursor-not-allowed; + } + + .input:focus, + .input-wrapper:focus-within { + @apply outline-none ring border-gray-400; + } + + .input-error { + @apply border-red-200; + } +} + +@layer utilities { + .h-input { + @apply h-[2.375rem]; + } } /** diff --git a/client/web/src/ui/collapsible.tsx b/client/web/src/ui/collapsible.tsx new file mode 100644 index 000000000..51faa9aad --- /dev/null +++ b/client/web/src/ui/collapsible.tsx @@ -0,0 +1,33 @@ +import * as Primitive from "@radix-ui/react-collapsible" +import React, { useState } from "react" +import { ReactComponent as ChevronDown } from "src/assets/icons/chevron-down.svg" + +type CollapsibleProps = { + trigger?: string + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export default function Collapsible(props: CollapsibleProps) { + const { children, trigger, onOpenChange } = props + const [open, setOpen] = useState(props.open) + + return ( + { + setOpen(open) + onOpenChange?.(open) + }} + > + + + + + {trigger} + + {children} + + ) +} diff --git a/client/web/src/ui/input.tsx b/client/web/src/ui/input.tsx new file mode 100644 index 000000000..2cba2251e --- /dev/null +++ b/client/web/src/ui/input.tsx @@ -0,0 +1,41 @@ +import cx from "classnames" +import React, { InputHTMLAttributes } from "react" + +type Props = { + className?: string + inputClassName?: string + error?: boolean + suffix?: JSX.Element +} & InputHTMLAttributes + +// Input is styled in a way that only works for text inputs. +const Input = React.forwardRef((props, ref) => { + const { + className, + inputClassName, + error, + prefix, + suffix, + disabled, + ...rest + } = props + return ( +
+ + {suffix ? ( +
+ {suffix} +
+ ) : null} +
+ ) +}) + +export default Input diff --git a/client/web/tailwind.config.js b/client/web/tailwind.config.js index c59f4cf0b..2cd07fa28 100644 --- a/client/web/tailwind.config.js +++ b/client/web/tailwind.config.js @@ -1,9 +1,8 @@ +const plugin = require("tailwindcss/plugin") + /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { fontFamily: { sans: [ @@ -32,6 +31,16 @@ module.exports = { }, extend: {}, }, - plugins: [], + plugins: [ + plugin(function ({ addVariant }) { + addVariant("state-open", [ + '&[data-state="open"]', + '[data-state="open"] &', + ]) + addVariant("state-closed", [ + '&[data-state="closed"]', + '[data-state="closed"] &', + ]) + }), + ], } - diff --git a/client/web/web.go b/client/web/web.go index 404d2a945..8929580c2 100644 --- a/client/web/web.go +++ b/client/web/web.go @@ -790,10 +790,18 @@ func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, opt tails go func() { if !isRunning { - s.lc.Start(ctx, ipn.Options{}) + ipnOptions := ipn.Options{AuthKey: opt.AuthKey} + if opt.ControlURL != "" { + ipnOptions.UpdatePrefs = &ipn.Prefs{ControlURL: opt.ControlURL} + } + if err := s.lc.Start(ctx, ipnOptions); err != nil { + s.logf("start: %v", err) + } } if opt.Reauthenticate { - s.lc.StartLoginInteractive(ctx) + if err := s.lc.StartLoginInteractive(ctx); err != nil { + s.logf("startLogin: %v", err) + } } }() @@ -802,6 +810,9 @@ func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, opt tails if err != nil { return "", err } + if n.State != nil && *n.State == ipn.Running { + return "", nil + } if n.ErrMessage != nil { msg := *n.ErrMessage return "", fmt.Errorf("backend error: %v", msg) @@ -816,6 +827,9 @@ type tailscaleUpOptions struct { // If true, force reauthentication of the client. // Otherwise simply reconnect, the same as running `tailscale up`. Reauthenticate bool + + ControlURL string + AuthKey string } // serveTailscaleUp serves requests to /api/up. diff --git a/client/web/yarn.lock b/client/web/yarn.lock index 8ce8b9e70..50eaf3616 100644 --- a/client/web/yarn.lock +++ b/client/web/yarn.lock @@ -478,6 +478,21 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" +"@radix-ui/react-collapsible@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz#df0e22e7a025439f13f62d4e4a9e92c4a0df5b81" + integrity sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-compose-refs@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"