client/web: style tweaks

Style changes made in live pairing session.

Updates #10261

Co-authored-by: Will Norris <will@tailscale.com>
Co-authored-by: Alessandro Mingione <alessandro@tailscale.com>
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
pull/10462/head
Sonia Appasamy 12 months ago committed by Sonia Appasamy
parent 1a4d423328
commit 014ae98297

@ -21,12 +21,14 @@ export default function AddressCard({
v6Address, v6Address,
shortDomain, shortDomain,
fullDomain, fullDomain,
className,
triggerClassName, triggerClassName,
}: { }: {
v4Address: string v4Address: string
v6Address: string v6Address: string
shortDomain?: string shortDomain?: string
fullDomain?: string fullDomain?: string
className?: string
triggerClassName?: string triggerClassName?: string
}) { }) {
const children = ( const children = (
@ -57,7 +59,7 @@ export default function AddressCard({
<Primitive.Trigger asChild> <Primitive.Trigger asChild>
<Button <Button
variant="minimal" variant="minimal"
className="-ml-1 px-1 py-0 hover:!bg-transparent font-normal" className={cx("-ml-1 px-1 py-0 font-normal", className)}
suffixIcon={ suffixIcon={
<ChevronDown className="w-5 h-5" stroke="#232222" /* gray-800 */ /> <ChevronDown className="w-5 h-5" stroke="#232222" /* gray-800 */ />
} }

@ -22,7 +22,7 @@ export default function App() {
const { data: auth, loading: loadingAuth, newSession } = useAuth() const { data: auth, loading: loadingAuth, newSession } = useAuth()
return ( return (
<main className="min-w-sm max-w-lg mx-auto py-4 md:py-14 px-5"> <main className="min-w-sm max-w-lg mx-auto py-4 sm:py-14 px-5">
{loadingAuth || !auth ? ( {loadingAuth || !auth ? (
<div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view <div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
) : ( ) : (
@ -136,24 +136,21 @@ function Header({
auth: AuthResponse auth: AuthResponse
newSession: () => Promise<void> newSession: () => Promise<void>
}) { }) {
const [loc, setLocation] = useLocation() const [loc] = useLocation()
return ( return (
<> <>
<div className="flex justify-between items-center mb-9 md:mb-12"> <div className="flex flex-wrap gap-4 justify-between items-center mb-9 md:mb-12">
<div className="flex gap-3"> <Link to="/" className="flex gap-3 overflow-hidden">
<TailscaleIcon <TailscaleIcon />
className="cursor-pointer" <div className="inline text-gray-800 text-lg font-medium leading-snug truncate">
onClick={() => setLocation("/")}
/>
<div className="inline text-gray-800 text-lg font-medium leading-snug">
{node.DomainName} {node.DomainName}
</div> </div>
</div> </Link>
<LoginToggle node={node} auth={auth} newSession={newSession} /> <LoginToggle node={node} auth={auth} newSession={newSession} />
</div> </div>
{loc !== "/" && loc !== "/update" && ( {loc !== "/" && loc !== "/update" && (
<Link to="/" className="link font-medium block mb-[10px]"> <Link to="/" className="link font-medium block mb-2">
&larr; Back to {node.DeviceName} &larr; Back to {node.DeviceName}
</Link> </Link>
)} )}

@ -58,38 +58,38 @@ export default function ExitNodeSelector({
) )
return ( return (
<Popover <div
open={disabled ? false : open} className={cx(
onOpenChange={setOpen} "rounded-md",
side="bottom" {
sideOffset={5} "bg-red-600": offline,
align="start" },
alignOffset={8} className
content={ )}
<ExitNodeSelectorInner
node={node}
selected={selected}
onSelect={handleSelect}
/>
}
asChild
> >
<div <div
className={cx( className={cx("p-1.5 rounded-md border flex items-stretch gap-1.5", {
"rounded-md", "border-gray-200": none,
{ "bg-yellow-300 border-yellow-300": advertising && !offline,
"bg-red-600": offline, "bg-blue-500 border-blue-500": using && !offline,
}, "bg-red-500 border-red-500": offline,
className })}
)}
> >
<div <Popover
className={cx("p-1.5 rounded-md border flex items-stretch gap-1.5", { open={disabled ? false : open}
"border-gray-200": none, onOpenChange={setOpen}
"bg-yellow-300 border-yellow-300": advertising && !offline, className="overflow-hidden"
"bg-blue-500 border-blue-500": using && !offline, side="bottom"
"bg-red-500 border-red-500": offline, sideOffset={0}
})} align="start"
content={
<ExitNodeSelectorInner
node={node}
selected={selected}
onSelect={handleSelect}
/>
}
asChild
> >
<button <button
className={cx("flex-1 px-2 py-1.5 rounded-[1px]", { className={cx("flex-1 px-2 py-1.5 rounded-[1px]", {
@ -108,7 +108,7 @@ export default function ExitNodeSelector({
<p <p
className={cx( className={cx(
"text-gray-500 text-xs text-left font-medium uppercase tracking-wide mb-1", "text-gray-500 text-xs text-left font-medium uppercase tracking-wide mb-1",
{ "bg-opacity-70 text-white": advertising || using } { "opacity-70 text-white": advertising || using }
)} )}
> >
Exit node{offline && " offline"} Exit node{offline && " offline"}
@ -138,32 +138,31 @@ export default function ExitNodeSelector({
)} )}
</div> </div>
</button> </button>
{!disabled && (advertising || using) && ( </Popover>
<button {!disabled && (advertising || using) && (
className={cx("px-3 py-2 rounded-sm text-white", { <button
"bg-yellow-200": advertising && !offline, className={cx("px-3 py-2 rounded-sm text-white", {
"bg-blue-400": using && !offline, "hover:bg-yellow-200": advertising && !offline,
"bg-red-400": offline, "hover:bg-blue-400": using && !offline,
})} "hover:bg-red-400": offline,
onClick={(e) => { })}
e.preventDefault() onClick={(e) => {
e.stopPropagation() e.preventDefault()
handleSelect(noExitNode) e.stopPropagation()
}} handleSelect(noExitNode)
> }}
Disable >
</button> Disable
)} </button>
</div>
{offline && (
<p className="text-white p-3">
The selected exit node is currently offline. Your internet traffic
is blocked until you disable the exit node or select a different
one.
</p>
)} )}
</div> </div>
</Popover> {offline && (
<p className="text-white p-3">
The selected exit node is currently offline. Your internet traffic is
blocked until you disable the exit node or select a different one.
</p>
)}
</div>
) )
} }
@ -205,10 +204,11 @@ function ExitNodeSelectorInner({
) )
return ( return (
<div className="w-[calc(var(--radix-popover-trigger-width)-16px)] pb-1 rounded-lg shadow"> <div className="w-[var(--radix-popover-trigger-width)]">
<SearchInput <SearchInput
name="exit-node-search" name="exit-node-search"
inputClassName="w-full px-4 py-2 border-none rounded-b-none" className="px-2"
inputClassName="w-full py-3 !h-auto border-none rounded-b-none !ring-0"
autoFocus autoFocus
autoCorrect="off" autoCorrect="off"
autoComplete="off" autoComplete="off"
@ -224,7 +224,7 @@ function ExitNodeSelectorInner({
{/* TODO(sonia): use loading spinner when loading useExitNodes */} {/* TODO(sonia): use loading spinner when loading useExitNodes */}
<div <div
ref={listRef} ref={listRef}
className="pt-1 border-t border-gray-200 max-h-64 overflow-y-scroll" className="pt-1 border-t border-gray-200 max-h-60 overflow-y-scroll"
> >
{hasNodes ? ( {hasNodes ? (
exitNodes.map( exitNodes.map(

@ -57,7 +57,7 @@ export default function LoginToggle({
) : ( ) : (
<div <div
className={cx( className={cx(
"w-[34px] h-[34px] p-1 rounded-full justify-center items-center inline-flex", "w-[34px] h-[34px] p-1 rounded-full justify-center items-center inline-flex hover:bg-gray-300",
{ {
"bg-transparent": !open, "bg-transparent": !open,
"bg-gray-300": open, "bg-gray-300": open,

@ -33,7 +33,7 @@ export default function HomeView({
<h2 className="mb-3">This device</h2> <h2 className="mb-3">This device</h2>
<div className="-mx-5 card mb-9"> <div className="-mx-5 card mb-9">
<div className="flex justify-between items-center text-lg mb-5"> <div className="flex justify-between items-center text-lg mb-5">
<div className="flex items-center"> <Link className="flex items-center" to="/details">
<div className="w-10 h-10 bg-gray-100 rounded-full justify-center items-center inline-flex"> <div className="w-10 h-10 bg-gray-100 rounded-full justify-center items-center inline-flex">
<Machine /> <Machine />
</div> </div>
@ -49,9 +49,10 @@ export default function HomeView({
{node.Status === "Running" ? "Connected" : "Offline"} {node.Status === "Running" ? "Connected" : "Offline"}
</p> </p>
</div> </div>
</div> </Link>
<AddressCard <AddressCard
triggerClassName="text-gray-800 text-lg leading-[25.20px]" className="-mr-2"
triggerClassName="relative text-gray-800 text-lg leading-[25.20px]"
v4Address={node.IPv4} v4Address={node.IPv4}
v6Address={node.IPv6} v6Address={node.IPv6}
shortDomain={node.DeviceName} shortDomain={node.DeviceName}
@ -150,7 +151,7 @@ function SettingsCard({
return ( return (
<button <button
className={cx("-mx-5 card cursor-pointer", className)} className={cx("-mx-5 card cursor-pointer", { "pb-4": footer }, className)}
onClick={() => setLocation(link)} onClick={() => setLocation(link)}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">

@ -4,6 +4,7 @@
import React from "react" import React from "react"
import * as Control from "src/components/control-components" import * as Control from "src/components/control-components"
import { NodeData, NodeUpdaters } from "src/hooks/node-data" import { NodeData, NodeUpdaters } from "src/hooks/node-data"
import Card from "src/ui/card"
import Toggle from "src/ui/toggle" import Toggle from "src/ui/toggle"
export default function SSHView({ export default function SSHView({
@ -30,23 +31,25 @@ export default function SSHView({
Learn more &rarr; Learn more &rarr;
</a> </a>
</p> </p>
<div className="-mx-5 px-4 py-3 bg-white rounded-lg border border-gray-200 flex gap-2.5 mb-3"> <Card className="-mx-5 p-4">
<Toggle <label className="flex gap-3 items-center">
checked={node.RunningSSHServer} <Toggle
onChange={() => checked={node.RunningSSHServer}
nodeUpdaters.patchPrefs({ onChange={() =>
RunSSHSet: true, nodeUpdaters.patchPrefs({
RunSSH: !node.RunningSSHServer, RunSSHSet: true,
}) RunSSH: !node.RunningSSHServer,
} })
disabled={readonly} }
/> disabled={readonly}
<div className="text-black text-sm font-medium leading-tight"> />
Run Tailscale SSH server <div className="text-black text-sm font-medium leading-tight">
</div> Run Tailscale SSH server
</div> </div>
</label>
</Card>
<Control.AdminContainer <Control.AdminContainer
className="text-gray-500 text-sm leading-tight" className="text-gray-500 text-sm leading-tight mt-3"
node={node} node={node}
> >
Remember to make sure that the{" "} Remember to make sure that the{" "}

@ -8,6 +8,8 @@ import { ReactComponent as Plus } from "src/assets/icons/plus.svg"
import * as Control from "src/components/control-components" import * as Control from "src/components/control-components"
import { NodeData, NodeUpdaters } from "src/hooks/node-data" import { NodeData, NodeUpdaters } from "src/hooks/node-data"
import Button from "src/ui/button" import Button from "src/ui/button"
import Card from "src/ui/card"
import EmptyState from "src/ui/empty-state"
import Input from "src/ui/input" import Input from "src/ui/input"
export default function SubnetRouterView({ export default function SubnetRouterView({
@ -50,7 +52,7 @@ export default function SubnetRouterView({
</p> </p>
{!readonly && {!readonly &&
(inputOpen ? ( (inputOpen ? (
<div className="-mx-5 card shadow"> <div className="-mx-5 card !border-0 shadow-popover">
<p className="font-medium leading-snug mb-3"> <p className="font-medium leading-snug mb-3">
Advertise new routes Advertise new routes
</p> </p>
@ -150,9 +152,9 @@ export default function SubnetRouterView({
)} )}
</> </>
) : ( ) : (
<div className="px-5 py-4 bg-gray-50 rounded-lg border border-gray-200 text-center text-gray-500"> <Card empty>
Not advertising any routes <EmptyState description="Not advertising any routes" />
</div> </Card>
)} )}
</div> </div>
</> </>

@ -226,6 +226,6 @@ export function trimDNSSuffix(s: string, tailnetDNSName: string): string {
export const noExitNode: ExitNode = { ID: "NONE", Name: "None", Online: true } export const noExitNode: ExitNode = { ID: "NONE", Name: "None", Online: true }
export const runAsExitNode: ExitNode = { export const runAsExitNode: ExitNode = {
ID: "RUNNING", ID: "RUNNING",
Name: "Run as exit node", Name: "Run as exit node",
Online: true, Online: true,
} }

@ -0,0 +1,40 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React from "react"
type Props = {
children: React.ReactNode
className?: string
elevated?: boolean
empty?: boolean
noPadding?: boolean
}
/**
* Card is a box with a border, rounded corners, and some padding. Use it to
* group content into a single container and give it more importance. The
* elevation prop gives it a box shadow, while the empty prop a light gray
* background color.
*
* <Card>{content}</Card>
* <Card elevated>{content}</Card>
* <Card empty><EmptyState description="You don't have any keys" /></Card>
*
*/
export default function Card(props: Props) {
const { children, className, elevated, empty, noPadding } = props
return (
<div
className={cx("rounded-md border", className, {
"shadow-soft": elevated,
"bg-gray-0": empty,
"bg-white": !empty,
"p-6": !noPadding,
})}
>
{children}
</div>
)
}

@ -0,0 +1,44 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { cloneElement } from "react"
type Props = {
action?: React.ReactNode
className?: string
description: string
icon?: React.ReactElement
title?: string
}
/**
* EmptyState shows some text and an optional action when some area that can
* house content is empty (eg. no search results, empty tables).
*/
export default function EmptyState(props: Props) {
const { action, className, description, icon, title } = props
const iconColor = "text-gray-500"
const iconComponent = getIcon(icon, iconColor)
return (
<div
className={cx("flex justify-center", className, {
"flex-col items-center": action || icon || title,
})}
>
{icon && <div className="mb-2">{iconComponent}</div>}
{title && (
<h3 className="text-xl font-medium text-center mb-2">{title}</h3>
)}
<div className="w-full text-center max-w-xl text-gray-500">
{description}
</div>
{action && <div className="mt-3.5">{action}</div>}
</div>
)
}
function getIcon(icon: React.ReactElement | undefined, iconColor: string) {
return icon ? cloneElement(icon, { className: iconColor }) : null
}

@ -17,10 +17,10 @@ const SearchInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
const { className, inputClassName, ...rest } = props const { className, inputClassName, ...rest } = props
return ( return (
<div className={cx("relative", className)}> <div className={cx("relative", className)}>
<Search className="absolute w-[1.25em] h-full ml-2" /> <Search className="absolute text-gray-400 w-[1.25em] h-full ml-2" />
<input <input
type="text" type="text"
className={cx("input px-8", inputClassName)} className={cx("input pl-9 pr-8", inputClassName)}
ref={ref} ref={ref}
{...rest} {...rest}
/> />

Loading…
Cancel
Save