client/web: add readonly/manage toggle
Updates tailscale/corp#14335 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:

committed by
Sonia Appasamy

parent
c54d680682
commit
86c8ab7502
@ -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,
|
||||
}: {
|
||||
|
Reference in New Issue
Block a user