diff --git a/client/web/src/components/app.tsx b/client/web/src/components/app.tsx
index 28350e52e..eb403a5e7 100644
--- a/client/web/src/components/app.tsx
+++ b/client/web/src/components/app.tsx
@@ -1,30 +1,123 @@
import React from "react"
import { Footer, Header, IP, State } from "src/components/legacy"
-import useNodeData from "src/hooks/node-data"
+import useNodeData, { NodeData } from "src/hooks/node-data"
+import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
+import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
+import { ReactComponent as TailscaleLogo } from "src/icons/tailscale-logo.svg"
export default function App() {
// TODO(sonia): use isPosting value from useNodeData
// to fill loading states.
const { data, refreshData, updateNode } = useNodeData()
- return (
-
- {!data ? (
- // TODO(sonia): add a loading view
-
Loading...
+ if (!data) {
+ // TODO(sonia): add a loading view
+ return
Loading...
+ }
+
+ const needsLogin = data?.Status === "NeedsLogin" || data?.Status === "NoState"
+
+ return !needsLogin &&
+ (data.DebugMode === "login" || data.DebugMode === "full") ? (
+
+ {data.DebugMode === "login" ? (
+
) : (
- <>
-
-
-
-
-
-
- >
+
+ )}
+
+
+ ) : (
+ // Legacy client UI
+
+
+
+
+
+
+
+
+ )
+}
+
+function LoginView(props: NodeData) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Owned by
+
+
+ {/* TODO(sonia): support tagged node profile view more eloquently */}
+ {props.Profile.LoginName}
+
+
+
+
+
+
+
+
+ {props.DeviceName}
+
+
{props.IP}
+
+
+
Access
+
+
+ >
+ )
+}
+
+function ManageView(props: NodeData) {
+ return (
+
+
+
+
+
{props.Profile.LoginName}
+ {/* TODO(sonia): support tagged node profile view more eloquently */}
+
+
+
+
This device
+
+
+ Tailscale is up and running. You can connect to this device from devices
+ in your tailnet by using its name or IP address.
+
+
+ )
+}
+
+function ProfilePic({ url }: { url: string }) {
+ return (
+
+ {url ? (
+
+ ) : (
+
)}
)
diff --git a/client/web/src/components/legacy.tsx b/client/web/src/components/legacy.tsx
index 5d7e269f1..a6f8ca7de 100644
--- a/client/web/src/components/legacy.tsx
+++ b/client/web/src/components/legacy.tsx
@@ -282,14 +282,14 @@ export function State({
}
}
-export function Footer(props: { data: NodeData }) {
- const { data } = props
-
+export function Footer(props: { licensesURL: string; className?: string }) {
return (
-
+
Open Source Licenses
diff --git a/client/web/src/hooks/node-data.ts b/client/web/src/hooks/node-data.ts
index 53c92c6ec..316c69b64 100644
--- a/client/web/src/hooks/node-data.ts
+++ b/client/web/src/hooks/node-data.ts
@@ -15,6 +15,8 @@ export type NodeData = {
IsUnraid: boolean
UnraidToken: string
IPNVersion: string
+
+ DebugMode: "" | "login" | "full" // empty when not running in any debug mode
}
export type UserProfile = {
diff --git a/client/web/src/icons/connected-device.svg b/client/web/src/icons/connected-device.svg
new file mode 100644
index 000000000..74f6be8bc
--- /dev/null
+++ b/client/web/src/icons/connected-device.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/web/src/icons/tailscale-icon.svg b/client/web/src/icons/tailscale-icon.svg
new file mode 100644
index 000000000..d6052fe5e
--- /dev/null
+++ b/client/web/src/icons/tailscale-icon.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/web/src/icons/tailscale-logo.svg b/client/web/src/icons/tailscale-logo.svg
new file mode 100644
index 000000000..6d5c7ce0c
--- /dev/null
+++ b/client/web/src/icons/tailscale-logo.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/web/tsconfig.json b/client/web/tsconfig.json
index 51c3de9d8..f87dd3058 100644
--- a/client/web/tsconfig.json
+++ b/client/web/tsconfig.json
@@ -10,6 +10,7 @@
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"jsx": "react",
+ "types": ["vite-plugin-svgr/client", "vite/client"]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
diff --git a/client/web/web.go b/client/web/web.go
index 39f9bf354..302cf5d1a 100644
--- a/client/web/web.go
+++ b/client/web/web.go
@@ -35,8 +35,8 @@ import (
type Server struct {
lc *tailscale.LocalClient
- devMode bool
- loginOnly bool
+ devMode bool
+ tsDebugMode string
cgiMode bool
pathPrefix string
@@ -72,10 +72,10 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
}
s = &Server{
devMode: opts.DevMode,
- loginOnly: opts.LoginOnly,
lc: opts.LocalClient,
pathPrefix: opts.PathPrefix,
}
+ s.tsDebugMode = s.debugMode()
s.assetsHandler, cleanup = assetsHandler(opts.DevMode)
// Create handler for "/api" requests with CSRF protection.
@@ -84,7 +84,7 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
// The client is secured by limiting the interface it listens on,
// or by authenticating requests before they reach the web client.
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
- if opts.LoginOnly {
+ if s.tsDebugMode == "login" {
// For the login client, we don't serve the full web client API,
// only the login endpoints.
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
@@ -97,6 +97,20 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
return s, cleanup
}
+// debugMode returns the debug mode the web client is being run in.
+// The empty string is returned in the case that this instance is
+// not running in any debug mode.
+func (s *Server) debugMode() string {
+ if !s.devMode {
+ return "" // debug modes only available in dev
+ }
+ switch mode := os.Getenv("TS_DEBUG_WEB_CLIENT_MODE"); mode {
+ case "login", "full": // valid debug modes
+ return mode
+ }
+ return ""
+}
+
// ServeHTTP processes all requests for the Tailscale web client.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := s.serve
@@ -153,7 +167,8 @@ func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
}
switch r.Method {
case httpm.GET:
- // TODO(soniaappasamy): implement
+ // TODO(soniaappasamy): we may want a minimal node data response here
+ s.serveGetNodeData(w, r)
case httpm.POST:
// TODO(soniaappasamy): implement
default:
@@ -206,6 +221,7 @@ type nodeData struct {
IsUnraid bool
UnraidToken string
IPNVersion string
+ DebugMode string // empty when not running in any debug mode
}
func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
@@ -233,6 +249,7 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
IsUnraid: distro.Get() == distro.Unraid,
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
IPNVersion: versionShort,
+ DebugMode: s.tsDebugMode,
}
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
exitNodeRouteV6 := netip.MustParsePrefix("::/0")