client/web: show features based on platform support

Hiding/disabling UI features when not available on the running
client.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy
2023-11-30 13:01:29 -05:00
committed by Sonia Appasamy
parent 7d61b827e8
commit 7a4ba609d9
14 changed files with 220 additions and 68 deletions

View File

@@ -11,7 +11,11 @@ import SSHView from "src/components/views/ssh-view"
import SubnetRouterView from "src/components/views/subnet-router-view"
import { UpdatingView } from "src/components/views/updating-view"
import useAuth, { AuthResponse } from "src/hooks/auth"
import useNodeData, { NodeData } from "src/hooks/node-data"
import useNodeData, {
Feature,
featureDescription,
NodeData,
} from "src/hooks/node-data"
import { Link, Route, Router, Switch, useLocation } from "wouter"
export default function App() {
@@ -63,27 +67,27 @@ function WebClient({
<Route path="/details">
<DeviceDetailsView readonly={!auth.canManageNode} node={data} />
</Route>
<Route path="/subnets">
<FeatureRoute path="/subnets" feature="advertise-routes" node={data}>
<SubnetRouterView
readonly={!auth.canManageNode}
node={data}
nodeUpdaters={nodeUpdaters}
/>
</Route>
<Route path="/ssh">
</FeatureRoute>
<FeatureRoute path="/ssh" feature="ssh" node={data}>
<SSHView
readonly={!auth.canManageNode}
node={data}
nodeUpdaters={nodeUpdaters}
/>
</Route>
</FeatureRoute>
<Route path="/serve">{/* TODO */}Share local content</Route>
<Route path="/update">
<FeatureRoute path="/update" feature="auto-update" node={data}>
<UpdatingView
versionInfo={data.ClientVersion}
currentVersion={data.IPNVersion}
/>
</Route>
</FeatureRoute>
<Route>
<h2 className="mt-8">Page not found</h2>
</Route>
@@ -93,6 +97,36 @@ function WebClient({
)
}
/**
* FeatureRoute renders a Route component,
* but only displays the child view if the specified feature is
* available for use on this node's platform. If not available,
* a not allowed view is rendered instead.
*/
function FeatureRoute({
path,
node,
feature,
children,
}: {
path: string
node: NodeData // TODO: once we have swr, just call useNodeData within FeatureView
feature: Feature
children: React.ReactNode
}) {
return (
<Route path={path}>
{!node.Features[feature] ? (
<h2 className="mt-8">
{featureDescription(feature)} not available on this device.
</h2>
) : (
children
)}
</Route>
)
}
function Header({
node,
auth,

View File

@@ -175,7 +175,7 @@ function ExitNodeSelectorInner({
onSelect: (node: ExitNode) => void
}) {
const [filter, setFilter] = useState<string>("")
const { data: exitNodes } = useExitNodes(node.TailnetName, filter)
const { data: exitNodes } = useExitNodes(node, filter)
const listRef = useRef<HTMLDivElement>(null)
const hasNodes = useMemo(

View File

@@ -50,9 +50,10 @@ export default function DeviceDetailsView({
</button>
</div>
</div>
{node.ClientVersion &&
!node.ClientVersion.RunningLatest &&
!readonly && (
{node.Features["auto-update"] &&
!readonly &&
node.ClientVersion &&
!node.ClientVersion.RunningLatest && (
<UpdateAvailableNotification details={node.ClientVersion} />
)}
<div className="-mx-5 card">

View File

@@ -33,37 +33,44 @@ export default function HomeView({
</div>
<p className="text-gray-800 text-lg leading-[25.20px]">{node.IP}</p>
</div>
<ExitNodeSelector
className="mb-5"
node={node}
nodeUpdaters={nodeUpdaters}
disabled={readonly}
/>
{(node.Features["advertise-exit-node"] ||
node.Features["use-exit-node"]) && (
<ExitNodeSelector
className="mb-5"
node={node}
nodeUpdaters={nodeUpdaters}
disabled={readonly}
/>
)}
<Link className="text-blue-500 font-medium leading-snug" to="/details">
View device details &rarr;
</Link>
</div>
<h2 className="mb-3">Settings</h2>
<SettingsCard
link="/subnets"
className="mb-3"
title="Subnet router"
body="Add devices to your tailnet without installing Tailscale on them."
/>
<SettingsCard
link="/ssh"
className="mb-3"
title="Tailscale SSH server"
body="Run a Tailscale SSH server on this device and allow other devices in your tailnet to SSH into it."
badge={
node.RunningSSHServer
? {
text: "Running",
icon: <div className="w-2 h-2 bg-emerald-500 rounded-full" />,
}
: undefined
}
/>
{node.Features["advertise-routes"] && (
<SettingsCard
link="/subnets"
className="mb-3"
title="Subnet router"
body="Add devices to your tailnet without installing Tailscale on them."
/>
)}
{node.Features["ssh"] && (
<SettingsCard
link="/ssh"
className="mb-3"
title="Tailscale SSH server"
body="Run a Tailscale SSH server on this device and allow other devices in your tailnet to SSH into it."
badge={
node.RunningSSHServer
? {
text: "Running",
icon: <div className="w-2 h-2 bg-emerald-500 rounded-full" />,
}
: undefined
}
/>
)}
{/* TODO(sonia,will): hiding unimplemented settings pages until implemented */}
{/* <SettingsCard
link="/serve"