client/web: build out client home page

Hooks up more of the home page UI.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
pull/10121/head
Sonia Appasamy 1 year ago committed by Sonia Appasamy
parent aba4bd0c62
commit f2a4c4fa55

@ -35,7 +35,7 @@ func startDevServer() (cleanup func()) {
node := filepath.Join(root, "tool", "node") node := filepath.Join(root, "tool", "node")
vite := filepath.Join(webClientPath, "node_modules", ".bin", "vite") vite := filepath.Join(webClientPath, "node_modules", ".bin", "vite")
log.Printf("installing JavaScript deps using %s... (might take ~30s)", yarn) log.Printf("installing JavaScript deps using %s...", yarn)
out, err := exec.Command(yarn, "--non-interactive", "-s", "--cwd", webClientPath, "install").CombinedOutput() out, err := exec.Command(yarn, "--non-interactive", "-s", "--cwd", webClientPath, "install").CombinedOutput()
if err != nil { if err != nil {
log.Fatalf("error running tailscale web's yarn install: %v, %s", err, out) log.Fatalf("error running tailscale web's yarn install: %v, %s", err, out)

@ -2,10 +2,10 @@ import cx from "classnames"
import React from "react" import React from "react"
import LegacyClientView from "src/components/views/legacy-client-view" import LegacyClientView from "src/components/views/legacy-client-view"
import LoginClientView from "src/components/views/login-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 ReadonlyClientView from "src/components/views/readonly-client-view"
import useAuth, { AuthResponse, SessionsCallbacks } from "src/hooks/auth" import useAuth, { AuthResponse, SessionsCallbacks } from "src/hooks/auth"
import useNodeData from "src/hooks/node-data" import useNodeData from "src/hooks/node-data"
import ManagementClientView from "./views/management-client-view"
export default function App() { export default function App() {
const { data: auth, loading: loadingAuth, sessions } = useAuth() const { data: auth, loading: loadingAuth, sessions } = useAuth()

@ -1,35 +1,107 @@
import cx from "classnames"
import React from "react" import React from "react"
import { NodeData } from "src/hooks/node-data" import { NodeData } from "src/hooks/node-data"
import { ReactComponent as ArrowRight } from "src/icons/arrow-right.svg"
import { ReactComponent as ChevronDown } from "src/icons/chevron-down.svg"
import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg" import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg" import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
import ProfilePic from "src/ui/profile-pic" import ProfilePic from "src/ui/profile-pic"
export default function ManagementClientView(props: NodeData) { export default function ManagementClientView(props: NodeData) {
return ( return (
<div className="px-5 mb-12"> <div className="px-5 mb-12 w-full">
<div className="flex justify-between mb-12"> <div className="flex justify-between mb-12">
<TailscaleIcon /> <TailscaleIcon />
<div className="flex"> <div className="flex">
<p className="mr-2">{props.Profile.LoginName}</p> <p className="mr-2">{props.Profile.LoginName}</p>
{/* TODO(sonia): support tagged node profile view more eloquently */}
<ProfilePic url={props.Profile.ProfilePicURL} /> <ProfilePic url={props.Profile.ProfilePicURL} />
</div> </div>
</div> </div>
<p className="tracking-wide uppercase text-gray-600 pb-3">This device</p>
<div className="-mx-5 border rounded-md px-5 py-4 bg-white"> <h1 className="mb-3">This device</h1>
<div className="flex justify-between items-center text-lg">
<div className="-mx-5 card mb-9">
<div className="flex justify-between items-center text-lg mb-5">
<div className="flex items-center"> <div className="flex items-center">
<ConnectedDeviceIcon /> <ConnectedDeviceIcon />
<p className="font-medium ml-3">{props.DeviceName}</p> <div className="ml-3">
<p className="text-neutral-800 text-lg font-medium leading-snug">
{props.DeviceName}
</p>
{/* TODO(sonia): display actual status */}
<p className="text-neutral-500 text-sm">Connected</p>
</div>
</div> </div>
<p className="tracking-widest">{props.IP}</p> <p className="text-neutral-800 text-lg leading-[25.20px]">
{props.IP}
</p>
</div> </div>
<ExitNodeSelector className="mb-5" />
<a className="text-indigo-500 font-medium leading-snug">
View device details &rarr;
</a>
</div>
<h1 className="mb-3">Settings</h1>
<SettingsCard
className="mb-3"
title="Subnet router"
body="Add devices to your tailnet without installing Tailscale on them."
/>
<SettingsCard
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."
/>
<SettingsCard
title="Share local content"
body="Share local ports, services, and content to your Tailscale network or to the broader internet."
/>
</div>
)
}
function ExitNodeSelector({ className }: { className?: string }) {
return (
<div className={cx("p-1.5 rounded-md border border-gray-200", className)}>
<div className="hover-button">
<p className="text-neutral-500 text-xs font-medium uppercase tracking-wide mb-1">
Exit node
</p>
<div className="flex items-center">
<p className="text-neutral-800">None</p>
<ChevronDown className="ml-[9px]" />
</div>
</div>
</div>
)
}
function SettingsCard({
title,
body,
className,
}: {
title: string
body: string
className?: string
}) {
return (
<div
className={cx(
"-mx-5 card flex justify-between items-center cursor-pointer",
className
)}
>
<div>
<p className="text-neutral-800 font-medium leading-tight mb-2">
{title}
</p>
<p className="text-neutral-500 text-sm leading-tight">{body}</p>
</div>
<div>
<ArrowRight className="ml-3" />
</div> </div>
<p className="text-gray-500 pt-2">
Tailscale is up and running. You can connect to this device from devices
in your tailnet by using its name or IP address.
</p>
<button className="button button-blue mt-6">Advertise exit node</button>
</div> </div>
) )
} }

@ -0,0 +1,4 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12.5H19" stroke="#706E6D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 5.5L19 12.5L12 19.5" stroke="#706E6D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 324 B

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 7.5L10 12.5L15 7.5" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 220 B

@ -2,6 +2,25 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
h1 {
@apply text-neutral-500 text-sm font-medium uppercase leading-tight tracking-wide;
}
}
@layer components {
.card {
@apply p-5 bg-white rounded-lg border border-gray-200;
}
.hover-button {
@apply px-2 py-1.5 bg-white rounded-[1px] cursor-pointer;
}
.hover-button:hover {
@apply bg-stone-100;
}
}
/** /**
* Non-Tailwind styles begin here. * Non-Tailwind styles begin here.
*/ */

Loading…
Cancel
Save