diff --git a/client/web/src/components/app.tsx b/client/web/src/components/app.tsx
index 004bc39c5..cf06ee724 100644
--- a/client/web/src/components/app.tsx
+++ b/client/web/src/components/app.tsx
@@ -1,35 +1,37 @@
+import cx from "classnames"
import React from "react"
-import { Footer, Header, IP, State } from "src/components/legacy"
-import useAuth, { AuthResponse } from "src/hooks/auth"
+import LegacyClientView from "src/components/views/legacy-client-view"
+import LoginClientView from "src/components/views/login-client-view"
+import ReadonlyClientView from "src/components/views/readonly-client-view"
+import useAuth from "src/hooks/auth"
import useNodeData, { NodeData } from "src/hooks/node-data"
-import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
-import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
-import { ReactComponent as TailscaleLogo } from "src/icons/tailscale-logo.svg"
+import ManagementClientView from "./views/management-client-view"
export default function App() {
- // TODO(sonia): use isPosting value from useNodeData
- // to fill loading states.
const { data, refreshData, updateNode } = useNodeData()
- if (!data) {
- // TODO(sonia): add a loading view
- return
- This device is authorized, but needs approval from a network admin
- before it can connect to the network.
-
- )
- default:
- return (
- <>
-
-
- You are connected! Access this device over Tailscale using the
- device name or IP address above.
-
-
-
- >
- )
- }
-}
-
-export function Footer(props: { licensesURL: string; className?: string }) {
- return (
-
- )
-}
diff --git a/client/web/src/components/views/login-client-view.tsx b/client/web/src/components/views/login-client-view.tsx
new file mode 100644
index 000000000..9133a6715
--- /dev/null
+++ b/client/web/src/components/views/login-client-view.tsx
@@ -0,0 +1,65 @@
+import React from "react"
+import { NodeData } from "src/hooks/node-data"
+import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
+
+/**
+ * LoginClientView is rendered when the client is not authenticated
+ * to a tailnet.
+ */
+export default function LoginClientView({
+ data,
+ onLoginClick,
+}: {
+ data: NodeData
+ onLoginClick: () => void
+}) {
+ return (
+
+
+ {data.IP ? (
+ <>
+
+
+ Your device's key has expired. Reauthenticate this device by
+ logging in again, or{" "}
+
+ learn more
+
+ .
+
+
+
+ >
+ ) : (
+ <>
+
+
Log in
+
+ Get started by logging in to your Tailscale network.
+ Or, learn more at{" "}
+
+ tailscale.com
+
+ .
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/client/web/src/components/views/management-client-view.tsx b/client/web/src/components/views/management-client-view.tsx
new file mode 100644
index 000000000..aa26b258b
--- /dev/null
+++ b/client/web/src/components/views/management-client-view.tsx
@@ -0,0 +1,35 @@
+import React from "react"
+import { NodeData } from "src/hooks/node-data"
+import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
+import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
+import ProfilePic from "src/ui/profile-pic"
+
+export default function ManagementClientView(props: NodeData) {
+ return (
+
+
+
+
+
{props.Profile.LoginName}
+ {/* TODO(sonia): support tagged node profile view more eloquently */}
+
+
+
+
This device
+
+
+
+
+
{props.DeviceName}
+
+
{props.IP}
+
+
+
+ Tailscale is up and running. You can connect to this device from devices
+ in your tailnet by using its name or IP address.
+
+
+
+ )
+}
diff --git a/client/web/src/components/views/readonly-client-view.tsx b/client/web/src/components/views/readonly-client-view.tsx
new file mode 100644
index 000000000..74ced9ee7
--- /dev/null
+++ b/client/web/src/components/views/readonly-client-view.tsx
@@ -0,0 +1,69 @@
+import React from "react"
+import { AuthResponse } from "src/hooks/auth"
+import { NodeData } from "src/hooks/node-data"
+import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
+import { ReactComponent as TailscaleLogo } from "src/icons/tailscale-logo.svg"
+import ProfilePic from "src/ui/profile-pic"
+
+/**
+ * ReadonlyClientView is rendered when the web interface is either
+ *
+ * 1. being viewed by a user not allowed to manage the node
+ * (e.g. user does not own the node)
+ *
+ * 2. or the user is allowed to manage the node but does not
+ * yet have a valid browser session.
+ */
+export default function ReadonlyClientView({
+ data,
+ auth,
+ waitOnAuth,
+}: {
+ data: NodeData
+ auth?: AuthResponse
+ waitOnAuth: () => Promise
+}) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Managed by
+
+
+ {/* TODO(sonia): support tagged node profile view more eloquently */}
+ {data.Profile.LoginName}
+