client/web: add readonly/manage toggle

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy
2023-11-09 16:19:22 -05:00
committed by Sonia Appasamy
parent c54d680682
commit 86c8ab7502
18 changed files with 731 additions and 189 deletions

View File

@ -1,22 +1,21 @@
import cx from "classnames"
import React, { useEffect } from "react"
import LoginToggle from "src/components/login-toggle"
import DeviceDetailsView from "src/components/views/device-details-view"
import HomeView from "src/components/views/home-view"
import LegacyClientView from "src/components/views/legacy-client-view"
import LoginClientView from "src/components/views/login-client-view"
import ManagementClientView from "src/components/views/management-client-view"
import ReadonlyClientView from "src/components/views/readonly-client-view"
import useAuth, { AuthResponse } from "src/hooks/auth"
import useNodeData, { NodeData, NodeUpdate } from "src/hooks/node-data"
import useNodeData, { NodeData } from "src/hooks/node-data"
import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
import ProfilePic from "src/ui/profile-pic"
import { Link, Route, Router, Switch, useLocation } from "wouter"
import DeviceDetailsView from "./views/device-details-view"
export default function App() {
const { data: auth, loading: loadingAuth, newSession } = useAuth()
return (
<main className="min-w-sm max-w-lg mx-auto py-14 px-5">
{loadingAuth ? (
{loadingAuth || !auth ? (
<div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
) : (
<WebClient auth={auth} newSession={newSession} />
@ -29,7 +28,7 @@ function WebClient({
auth,
newSession,
}: {
auth?: AuthResponse
auth: AuthResponse
newSession: () => Promise<void>
}) {
const { data, refreshData, updateNode } = useNodeData()
@ -37,36 +36,44 @@ function WebClient({
refreshData()
}, [auth, refreshData])
if (!data) {
return <div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
}
return (
return !data ? (
<div className="text-center py-14">Loading...</div>
) : data.Status === "NeedsLogin" || data.Status === "NoState" ? (
// Client not on a tailnet, render login.
<LoginClientView
data={data}
onLoginClick={() => updateNode({ Reauthenticate: true })}
/>
) : data.DebugMode !== "full" && data.DebugMode !== "login" ? (
// Render legacy client interface.
<>
{/* TODO(sonia): get rid of the conditions here once full/readonly
* views live on same components */}
{data.DebugMode === "full" && auth?.ok && <Header node={data} />}
<LegacyClientView
data={data}
refreshData={refreshData}
updateNode={updateNode}
/>
{/* TODO: add license to new client */}
<Footer licensesURL={data.LicensesURL} />
</>
) : (
// Otherwise render the new web client.
<>
<Header node={data} auth={auth} newSession={newSession} />
<Router base={data.URLPrefix}>
<Switch>
<Route path="/">
<HomeView
auth={auth}
data={data}
newSession={newSession}
refreshData={refreshData}
readonly={!auth.canManageNode}
node={data}
updateNode={updateNode}
/>
</Route>
{data.DebugMode !== "" && (
<>
<Route path="/details">
<DeviceDetailsView node={data} />
</Route>
<Route path="/subnets">{/* TODO */}Subnet router</Route>
<Route path="/ssh">{/* TODO */}Tailscale SSH server</Route>
<Route path="/serve">{/* TODO */}Share local content</Route>
</>
)}
<Route path="/details">
<DeviceDetailsView readonly={!auth.canManageNode} node={data} />
</Route>
<Route path="/subnets">{/* TODO */}Subnet router</Route>
<Route path="/ssh">{/* TODO */}Tailscale SSH server</Route>
<Route path="/serve">{/* TODO */}Share local content</Route>
<Route>
<h2 className="mt-8">Page not found</h2>
</Route>
@ -76,57 +83,27 @@ function WebClient({
)
}
function HomeView({
function Header({
node,
auth,
data,
newSession,
refreshData,
updateNode,
}: {
auth?: AuthResponse
data: NodeData
node: NodeData
auth: AuthResponse
newSession: () => Promise<void>
refreshData: () => Promise<void>
updateNode: (update: NodeUpdate) => Promise<void> | undefined
}) {
return (
<>
{data?.Status === "NeedsLogin" || data?.Status === "NoState" ? (
// Client not on a tailnet, render login.
<LoginClientView
data={data}
onLoginClick={() => updateNode({ Reauthenticate: true })}
/>
) : data.DebugMode === "full" && auth?.ok ? (
// Render new client interface in management mode.
<ManagementClientView node={data} updateNode={updateNode} />
) : data.DebugMode === "login" || data.DebugMode === "full" ? (
// Render new client interface in readonly mode.
<ReadonlyClientView data={data} auth={auth} newSession={newSession} />
) : (
// Render legacy client interface.
<LegacyClientView
data={data}
refreshData={refreshData}
updateNode={updateNode}
/>
)}
{<Footer licensesURL={data.LicensesURL} />}
</>
)
}
function Header({ node }: { node: NodeData }) {
const [loc] = useLocation()
return (
<>
<div className="flex justify-between mb-12">
<TailscaleIcon />
<div className="flex">
<p className="mr-2">{node.Profile.LoginName}</p>
<ProfilePic url={node.Profile.ProfilePicURL} />
<div className="flex gap-3">
<TailscaleIcon />
<div className="inline text-neutral-800 text-lg font-medium leading-snug">
{node.DomainName}
</div>
</div>
<LoginToggle node={node} auth={auth} newSession={newSession} />
</div>
{loc !== "/" && (
<Link
@ -140,7 +117,7 @@ function Header({ node }: { node: NodeData }) {
)
}
export function Footer({
function Footer({
licensesURL,
className,
}: {