From d544e80fc11c9ff58bf9f3cb2273726d69c26232 Mon Sep 17 00:00:00 2001 From: Sonia Appasamy Date: Wed, 8 Nov 2023 17:33:27 -0500 Subject: [PATCH] client/web: populate device details view Fills /details page with real values, passed back from the /data endpoint. Updates tailscale/corp#14335 Signed-off-by: Sonia Appasamy --- client/web/src/components/acl-tag.tsx | 25 +++++++ .../components/views/device-details-view.tsx | 67 ++++++++++------- client/web/src/hooks/node-data.ts | 18 ++++- client/web/src/index.css | 2 +- client/web/src/ui/badge.tsx | 48 ++++++++++++ client/web/web.go | 75 +++++++++++++------ 6 files changed, 182 insertions(+), 53 deletions(-) create mode 100644 client/web/src/components/acl-tag.tsx create mode 100644 client/web/src/ui/badge.tsx diff --git a/client/web/src/components/acl-tag.tsx b/client/web/src/components/acl-tag.tsx new file mode 100644 index 000000000..94522befe --- /dev/null +++ b/client/web/src/components/acl-tag.tsx @@ -0,0 +1,25 @@ +import cx from "classnames" +import React from "react" +import Badge from "src/ui/badge" + +/** + * ACLTag handles the display of an ACL tag. + */ +export default function ACLTag({ + tag, + className, +}: { + tag: string + className?: string +}) { + return ( + + tag: + {tag.replace("tag:", "")} + + ) +} diff --git a/client/web/src/components/views/device-details-view.tsx b/client/web/src/components/views/device-details-view.tsx index 513c938bc..3760f90e5 100644 --- a/client/web/src/components/views/device-details-view.tsx +++ b/client/web/src/components/views/device-details-view.tsx @@ -1,8 +1,13 @@ +import cx from "classnames" import React from "react" +import { apiFetch } from "src/api" import { NodeData } from "src/hooks/node-data" -import ProfilePic from "src/ui/profile-pic" +import { useLocation } from "wouter" +import ACLTag from "../acl-tag" export default function DeviceDetailsView({ node }: { node: NodeData }) { + const [, setLocation] = useLocation() + return (

Device details

@@ -11,37 +16,36 @@ export default function DeviceDetailsView({ node }: { node: NodeData }) {

{node.DeviceName}

- {/* TODO: connected status */} -
+
-
-
-
- Managed by -
-
- {/* TODO: tags display */} - -
- {node.Profile.LoginName} -
-

General

- {/* TODO: pipe through these values */} - - - - - + - + @@ -49,11 +53,11 @@ export default function DeviceDetailsView({ node }: { node: NodeData }) { - + - + @@ -61,7 +65,12 @@ export default function DeviceDetailsView({ node }: { node: NodeData }) { - +
Creator{node.Profile.DisplayName}
Managed by{node.Profile.DisplayName} + {node.IsTagged + ? node.Tags.map((t) => ) + : node.Profile.DisplayName} +
Machine name
OSMacOS{node.OS}
IDnPKyyg3CNTRL{node.ID}
Tailscale version
Key expiry3 months from now + {node.KeyExpired + ? "Expired" + : // TODO: present as relative expiry (e.g. "5 months from now") + new Date(node.KeyExpiry).toLocaleString()} +
@@ -76,7 +85,7 @@ export default function DeviceDetailsView({ node }: { node: NodeData }) { Tailscale IPv6 - fd7a:115c:a1e0:ab12:4843:cd96:627a:f179 + {node.IPv6} Short domain @@ -84,7 +93,9 @@ export default function DeviceDetailsView({ node }: { node: NodeData }) { Full domain - {node.DeviceName}.corp.ts.net + + {node.DeviceName}.{node.TailnetName} + diff --git a/client/web/src/hooks/node-data.ts b/client/web/src/hooks/node-data.ts index 73657a19f..c2cfa5889 100644 --- a/client/web/src/hooks/node-data.ts +++ b/client/web/src/hooks/node-data.ts @@ -3,9 +3,14 @@ import { apiFetch, setUnraidCsrfToken } from "src/api" export type NodeData = { Profile: UserProfile - Status: string + Status: NodeState DeviceName: string + OS: string IP: string + IPv6: string + ID: string + KeyExpiry: string + KeyExpired: boolean AdvertiseExitNode: boolean AdvertiseRoutes: string LicensesURL: string @@ -16,10 +21,21 @@ export type NodeData = { UnraidToken: string IPNVersion: string URLPrefix: string + TailnetName: string + IsTagged: boolean + Tags: string[] DebugMode: "" | "login" | "full" // empty when not running in any debug mode } +type NodeState = + | "NoState" + | "NeedsLogin" + | "NeedsMachineAuth" + | "Stopped" + | "Starting" + | "Running" + export type UserProfile = { LoginName: string DisplayName: string diff --git a/client/web/src/index.css b/client/web/src/index.css index 0cd0aa15c..ff72ac040 100644 --- a/client/web/src/index.css +++ b/client/web/src/index.css @@ -26,7 +26,7 @@ @apply flex flex-col gap-2; } .card td:first-child { - @apply w-40 text-neutral-500 text-sm leading-tight; + @apply w-40 text-neutral-500 text-sm leading-tight flex-shrink-0; } .card td:last-child { @apply text-neutral-800 text-sm leading-tight; diff --git a/client/web/src/ui/badge.tsx b/client/web/src/ui/badge.tsx new file mode 100644 index 000000000..ca211822c --- /dev/null +++ b/client/web/src/ui/badge.tsx @@ -0,0 +1,48 @@ +import cx from "classnames" +import React, { HTMLAttributes } from "react" + +export type BadgeColor = + | "blue" + | "green" + | "red" + | "orange" + | "yellow" + | "gray" + | "outline" + +type Props = { + variant: "tag" | "status" + color: BadgeColor +} & HTMLAttributes + +export default function Badge(props: Props) { + const { className, color, variant, ...rest } = props + + return ( +
+ ) +} + +Badge.defaultProps = { + color: "gray", +} diff --git a/client/web/web.go b/client/web/web.go index 271b648ea..d45fd2d9d 100644 --- a/client/web/web.go +++ b/client/web/web.go @@ -490,21 +490,35 @@ func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) { } type nodeData struct { - Profile tailcfg.UserProfile - Status string - DeviceName string - IP string + ID tailcfg.StableNodeID + Status string + DeviceName string + TailnetName string // TLS cert name + IP string // IPv4 + IPv6 string + OS string + IPNVersion string + + Profile tailcfg.UserProfile + IsTagged bool + Tags []string + + KeyExpiry string // time.RFC3339 + KeyExpired bool + + TUNMode bool + IsSynology bool + DSMVersion int // 6 or 7, if IsSynology=true + IsUnraid bool + UnraidToken string + URLPrefix string // if set, the URL prefix the client is served behind + AdvertiseExitNode bool AdvertiseRoutes string - LicensesURL string - TUNMode bool - IsSynology bool - DSMVersion int // 6 or 7, if IsSynology=true - IsUnraid bool - UnraidToken string - IPNVersion string - DebugMode string // empty when not running in any debug mode - URLPrefix string // if set, the URL prefix the client is served behind + + LicensesURL string + + DebugMode string // empty when not running in any debug mode } func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) { @@ -518,9 +532,6 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - profile := st.User[st.Self.UserID] - deviceName := strings.Split(st.Self.DNSName, ".")[0] - versionShort := strings.Split(st.Version, "-")[0] var debugMode string if s.mode == ManageServerMode { debugMode = "full" @@ -528,19 +539,40 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) { debugMode = "login" } data := &nodeData{ - Profile: profile, + ID: st.Self.ID, Status: st.BackendState, - DeviceName: deviceName, - LicensesURL: licenses.LicensesURL(), + DeviceName: strings.Split(st.Self.DNSName, ".")[0], + TailnetName: st.CurrentTailnet.MagicDNSSuffix, + OS: st.Self.OS, + IPNVersion: strings.Split(st.Version, "-")[0], + Profile: st.User[st.Self.UserID], + IsTagged: st.Self.IsTagged(), + KeyExpired: st.Self.Expired, TUNMode: st.TUN, IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"), DSMVersion: distro.DSMVersion(), IsUnraid: distro.Get() == distro.Unraid, UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"), - IPNVersion: versionShort, URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"), + LicensesURL: licenses.LicensesURL(), DebugMode: debugMode, // TODO(sonia,will): just pass back s.mode directly? } + for _, ip := range st.TailscaleIPs { + if ip.Is4() { + data.IP = ip.String() + } else if ip.Is6() { + data.IPv6 = ip.String() + } + if data.IP != "" && data.IPv6 != "" { + break + } + } + if st.Self.Tags != nil { + data.Tags = st.Self.Tags.AsSlice() + } + if st.Self.KeyExpiry != nil { + data.KeyExpiry = st.Self.KeyExpiry.Format(time.RFC3339) + } for _, r := range prefs.AdvertiseRoutes { if r == exitNodeRouteV4 || r == exitNodeRouteV6 { data.AdvertiseExitNode = true @@ -551,9 +583,6 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) { data.AdvertiseRoutes += r.String() } } - if len(st.TailscaleIPs) != 0 { - data.IP = st.TailscaleIPs[0].String() - } writeJSON(w, *data) }