diff --git a/client/web/src/components/app.tsx b/client/web/src/components/app.tsx
index 2b885f14d..8fcdaf6f6 100644
--- a/client/web/src/components/app.tsx
+++ b/client/web/src/components/app.tsx
@@ -73,7 +73,7 @@ function WebClient({
diff --git a/client/web/src/components/control-components.tsx b/client/web/src/components/control-components.tsx
new file mode 100644
index 000000000..d961e0684
--- /dev/null
+++ b/client/web/src/components/control-components.tsx
@@ -0,0 +1,57 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+import React from "react"
+import { NodeData } from "src/hooks/node-data"
+
+/**
+ * AdminContainer renders its contents only if the node's control
+ * server has an admin panel.
+ *
+ * TODO(sonia,will): Similarly, this could also hide the contents
+ * if the viewing user is a non-admin.
+ */
+export function AdminContainer({
+ node,
+ children,
+ className,
+}: {
+ node: NodeData
+ children: React.ReactNode
+ className?: string
+}) {
+ if (!node.ControlAdminURL.includes("tailscale.com")) {
+ // Admin panel only exists on Tailscale control servers.
+ return null
+ }
+ return
@@ -119,19 +120,16 @@ export default function DeviceDetailsView({
-
+
Want even more details? Visit{" "}
-
+
this device’s page
- {" "}
+ {" "}
in the admin console.
-
+
>
)
diff --git a/client/web/src/components/views/ssh-view.tsx b/client/web/src/components/views/ssh-view.tsx
index d24fb6a83..1a80edf99 100644
--- a/client/web/src/components/views/ssh-view.tsx
+++ b/client/web/src/components/views/ssh-view.tsx
@@ -2,16 +2,17 @@
// SPDX-License-Identifier: BSD-3-Clause
import React from "react"
-import { NodeUpdaters } from "src/hooks/node-data"
+import * as Control from "src/components/control-components"
+import { NodeData, NodeUpdaters } from "src/hooks/node-data"
import Toggle from "src/ui/toggle"
export default function SSHView({
readonly,
- runningSSH,
+ node,
nodeUpdaters,
}: {
readonly: boolean
- runningSSH: boolean
+ node: NodeData
nodeUpdaters: NodeUpdaters
}) {
return (
@@ -31,9 +32,12 @@ export default function SSHView({
- nodeUpdaters.patchPrefs({ RunSSHSet: true, RunSSH: !runningSSH })
+ nodeUpdaters.patchPrefs({
+ RunSSHSet: true,
+ RunSSH: !node.RunningSSHServer,
+ })
}
disabled={readonly}
/>
@@ -41,18 +45,16 @@ export default function SSHView({
Run Tailscale SSH server
-
+
Remember to make sure that the{" "}
-
+
tailnet policy file
- {" "}
+ {" "}
allows other devices to SSH into this device.
-
+
>
)
}
diff --git a/client/web/src/components/views/subnet-router-view.tsx b/client/web/src/components/views/subnet-router-view.tsx
index d800a13c0..6f72c1875 100644
--- a/client/web/src/components/views/subnet-router-view.tsx
+++ b/client/web/src/components/views/subnet-router-view.tsx
@@ -5,6 +5,7 @@ import React, { useMemo, useState } from "react"
import { ReactComponent as CheckCircle } from "src/assets/icons/check-circle.svg"
import { ReactComponent as Clock } from "src/assets/icons/clock.svg"
import { ReactComponent as Plus } from "src/assets/icons/plus.svg"
+import * as Control from "src/components/control-components"
import { NodeData, NodeUpdaters } from "src/hooks/node-data"
import Button from "src/ui/button"
import Input from "src/ui/input"
@@ -122,18 +123,16 @@ export default function SubnetRouterView({
))}
-
+
>
) : (
diff --git a/client/web/src/hooks/node-data.ts b/client/web/src/hooks/node-data.ts
index 1a115985e..31884f5ee 100644
--- a/client/web/src/hooks/node-data.ts
+++ b/client/web/src/hooks/node-data.ts
@@ -19,7 +19,6 @@ export type NodeData = {
UsingExitNode?: ExitNode
AdvertisingExitNode: boolean
AdvertisedRoutes?: SubnetRoute[]
- LicensesURL: string
TUNMode: boolean
IsSynology: boolean
DSMVersion: number
@@ -33,6 +32,8 @@ export type NodeData = {
IsTagged: boolean
Tags: string[]
RunningSSHServer: boolean
+ ControlAdminURL: string
+ LicensesURL: string
}
type NodeState =
@@ -204,5 +205,10 @@ export default function useNodeData() {
]
)
- return { data, refreshData, nodeUpdaters, isPosting }
+ return {
+ data: { ...data, ControlAdminURL: "somehting.com" },
+ refreshData,
+ nodeUpdaters,
+ isPosting,
+ }
}
diff --git a/client/web/web.go b/client/web/web.go
index 46b4c7508..5bc45898d 100644
--- a/client/web/web.go
+++ b/client/web/web.go
@@ -561,7 +561,8 @@ type nodeData struct {
ClientVersion *tailcfg.ClientVersion
- LicensesURL string
+ ControlAdminURL string
+ LicensesURL string
}
type subnetRoute struct {
@@ -596,8 +597,10 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
RunningSSHServer: prefs.RunSSH,
URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"),
+ ControlAdminURL: prefs.AdminPageURL(),
LicensesURL: licenses.LicensesURL(),
}
+
cv, err := s.lc.CheckUpdate(r.Context())
if err != nil {
s.logf("could not check for updates: %v", err)
diff --git a/ipn/prefs.go b/ipn/prefs.go
index 71aef0733..5e8033a28 100644
--- a/ipn/prefs.go
+++ b/ipn/prefs.go
@@ -570,7 +570,7 @@ func (p *Prefs) AdminPageURL() string {
// TODO(crawshaw): In future release, make this https://console.tailscale.com
url = "https://login.tailscale.com"
}
- return url + "/admin/machines"
+ return url + "/admin"
}
// AdvertisesExitNode reports whether p is advertising both the v4 and