Skip to content

Commit

Permalink
client/web: add serve/funnel view
Browse files Browse the repository at this point in the history
Adds a new view to the web client for managing serve/funnel.
The view is permissioned by the "serve" and "funnel" grants,
and allows for http/https/tcp proxy and plain text serving.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
  • Loading branch information
soniaappasamy committed Mar 8, 2024
1 parent c58c59e commit ec6049f
Show file tree
Hide file tree
Showing 20 changed files with 7,473 additions and 34 deletions.
4 changes: 4 additions & 0 deletions client/web/auth.go
Expand Up @@ -281,6 +281,8 @@ const (
capFeatureSSH capFeature = "ssh" // grants peer SSH server management
capFeatureSubnets capFeature = "subnets" // grants peer subnet routes management
capFeatureExitNodes capFeature = "exitnodes" // grants peer ability to advertise-as and use exit nodes
capFeatureServe capFeature = "serve" // grants peer ability to share resources over Tailscale Serve
capFeatureFunnel capFeature = "funnel" // grants peer ability to share resources over Tailscale Funnel
capFeatureAccount capFeature = "account" // grants peer ability to turn on auto updates and log out of node
)

Expand All @@ -292,6 +294,8 @@ var validCaps []capFeature = []capFeature{
capFeatureSSH,
capFeatureSubnets,
capFeatureExitNodes,
capFeatureServe,
capFeatureFunnel,
capFeatureAccount,
}

Expand Down
2 changes: 2 additions & 0 deletions client/web/package.json
Expand Up @@ -11,7 +11,9 @@
"dependencies": {
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-tooltip": "^1.0.6",
"classnames": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
28 changes: 26 additions & 2 deletions client/web/src/api.ts
Expand Up @@ -3,7 +3,7 @@

import { useCallback } from "react"
import useToaster from "src/hooks/toaster"
import { ExitNode, NodeData, SubnetRoute } from "src/types"
import { ExitNode, NodeData, ServeData, SubnetRoute } from "src/types"
import { assertNever } from "src/utils/util"
import { MutatorOptions, SWRConfiguration, useSWRConfig } from "swr"
import { noExitNode, runAsExitNode } from "./hooks/exit-nodes"
Expand All @@ -20,6 +20,8 @@ type APIType =
| { action: "update-prefs"; data: LocalPrefsData }
| { action: "update-routes"; data: SubnetRoute[] }
| { action: "update-exit-node"; data: ExitNode }
| { action: "patch-serve-item"; data: ServeData } // add or update
| { action: "delete-serve-item"; data: ServeData }

/**
* POST /api/up data
Expand Down Expand Up @@ -239,6 +241,28 @@ export function useAPI() {
.catch(handlePostError("Failed to update exit node"))
}

/**
* "patch-serve-item" handles adding or updating an item in the
* node's serve config.
*/
case "patch-serve-item": {
// todo: report metric?
return apiFetch("/serve/items", "PATCH", t.data).catch(
handlePostError("Failed to update item")
)
}

/**
* "delete-serve-item" handles deleting an item in the node's
* serve config.
*/
case "delete-serve-item": {
// todo: report metric?
return apiFetch("/serve/items", "DELETE", t.data).catch(
handlePostError("Failed to delete item")
)
}

default:
assertNever(t)
}
Expand All @@ -263,7 +287,7 @@ let unraidCsrfToken: string | undefined // required for unraid POST requests (#8
*/
export function apiFetch<T>(
endpoint: string,
method: "GET" | "POST" | "PATCH",
method: "GET" | "POST" | "PATCH" | "DELETE",
body?: any
): Promise<T> {
const urlParams = new URLSearchParams(window.location.search)
Expand Down
4 changes: 2 additions & 2 deletions client/web/src/assets/icons/copy.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions client/web/src/assets/icons/globe.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions client/web/src/assets/icons/home.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion client/web/src/components/address-copy-card.tsx
Expand Up @@ -125,7 +125,7 @@ function AddressRow({
"text-gray-900 group-hover:text-gray-600"
)}
>
<Copy className="w-4 h-4" />
<Copy className="w-4 h-4" stroke="#292828" />
</span>
</button>
</li>
Expand Down
9 changes: 6 additions & 3 deletions client/web/src/components/app.tsx
Expand Up @@ -8,11 +8,12 @@ import DeviceDetailsView from "src/components/views/device-details-view"
import DisconnectedView from "src/components/views/disconnected-view"
import HomeView from "src/components/views/home-view"
import LoginView from "src/components/views/login-view"
import ServeView from "src/components/views/serve-view"
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, canEdit } from "src/hooks/auth"
import { Feature, NodeData, featureDescription } from "src/types"
import { Feature, NodeData, featureLongName } from "src/types"
import Card from "src/ui/card"
import EmptyState from "src/ui/empty-state"
import LoadingDots from "src/ui/loading-dots"
Expand Down Expand Up @@ -70,7 +71,9 @@ function WebClient({
<FeatureRoute path="/ssh" feature="ssh" node={node}>
<SSHView readonly={!canEdit("ssh", auth)} node={node} />
</FeatureRoute>
{/* <Route path="/serve">Share local content</Route> */}
<FeatureRoute path="/serve" feature="serve" node={node}>
<ServeView node={node} auth={auth} />
</FeatureRoute>
<FeatureRoute path="/update" feature="auto-update" node={node}>
<UpdatingView
versionInfo={node.ClientVersion}
Expand Down Expand Up @@ -113,7 +116,7 @@ function FeatureRoute({
{!node.Features[feature] ? (
<Card className="mt-8">
<EmptyState
description={`${featureDescription(
description={`${featureLongName(
feature
)} not available on this device.`}
/>
Expand Down
64 changes: 48 additions & 16 deletions client/web/src/components/views/home-view.tsx
Expand Up @@ -5,13 +5,15 @@ import cx from "classnames"
import React, { useMemo } from "react"
import { apiFetch } from "src/api"
import ArrowRight from "src/assets/icons/arrow-right.svg?react"
import Globe from "src/assets/icons/globe.svg?react"
import Machine from "src/assets/icons/machine.svg?react"
import AddressCard from "src/components/address-copy-card"
import ExitNodeSelector from "src/components/exit-node-selector"
import { AuthResponse, canEdit } from "src/hooks/auth"
import { NodeData } from "src/types"
import { NodeData, ServeData } from "src/types"
import Card from "src/ui/card"
import { pluralize } from "src/utils/util"
import useSWR from "swr"
import { Link, useLocation } from "wouter"

export default function HomeView({
Expand All @@ -21,10 +23,18 @@ export default function HomeView({
node: NodeData
auth: AuthResponse
}) {
const { data: serveData } = useSWR<ServeData[]>("/serve/items")
const [allServeAndFunnel, onlyFunnel] = useMemo(
() => [
serveData?.length || 0,
serveData?.filter((d) => d.shareType === "funnel").length || 0,
],
[serveData]
)
const [allSubnetRoutes, pendingSubnetRoutes] = useMemo(
() => [
node.AdvertisedRoutes?.length,
node.AdvertisedRoutes?.filter((r) => !r.Approved).length,
node.AdvertisedRoutes?.length || 0,
node.AdvertisedRoutes?.filter((r) => !r.Approved).length || 0,
],
[node.AdvertisedRoutes]
)
Expand Down Expand Up @@ -98,11 +108,13 @@ export default function HomeView({
}
footer={
pendingSubnetRoutes
? `${pendingSubnetRoutes} ${pluralize(
"route",
"routes",
pendingSubnetRoutes
)} pending approval`
? {
text: `${pendingSubnetRoutes} ${pluralize(
"route",
"routes",
pendingSubnetRoutes
)} pending approval`,
}
: undefined
}
/>
Expand All @@ -122,12 +134,26 @@ export default function HomeView({
}
/>
)}
{/* TODO(sonia,will): hiding unimplemented settings pages until implemented */}
{/* <SettingsCard
link="/serve"
title="Share local content"
body="Share local ports, services, and content to your Tailscale network or to the broader internet."
/> */}
{node.Features["serve"] && (
<SettingsCard
link="/serve"
title="Share local content"
body="Share local ports, services, and content to your Tailscale network or to the broader internet."
badge={
allServeAndFunnel > 0
? { text: `${allServeAndFunnel} shared` }
: undefined
}
footer={
onlyFunnel
? {
text: `${onlyFunnel} shared on the internet`,
icon: <Globe className="w-4 h-4 stroke-gray-500" />,
}
: undefined
}
/>
)}
</div>
</div>
)
Expand All @@ -148,7 +174,10 @@ function SettingsCard({
text: string
icon?: JSX.Element
}
footer?: string
footer?: {
text: string
icon?: JSX.Element
}
className?: string
}) {
const [, setLocation] = useLocation()
Expand Down Expand Up @@ -180,7 +209,10 @@ function SettingsCard({
{footer && (
<>
<hr className="my-3" />
<div className="text-gray-500 text-sm leading-tight">{footer}</div>
<div className="flex items-center gap-[6px] text-gray-500 text-sm leading-tight">
{footer.text}
{footer.icon}
</div>
</>
)}
</Card>
Expand Down

0 comments on commit ec6049f

Please sign in to comment.