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>
This commit is contained in:
Sonia Appasamy 2023-12-06 16:11:00 -05:00 committed by Sonia Appasamy
parent 1a4d423328
commit 014ae98297
11 changed files with 186 additions and 97 deletions

View File

@ -21,12 +21,14 @@ export default function AddressCard({
v6Address,
shortDomain,
fullDomain,
className,
triggerClassName,
}: {
v4Address: string
v6Address: string
shortDomain?: string
fullDomain?: string
className?: string
triggerClassName?: string
}) {
const children = (
@ -57,7 +59,7 @@ export default function AddressCard({
<Primitive.Trigger asChild>
<Button
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={
<ChevronDown className="w-5 h-5" stroke="#232222" /* gray-800 */ />
}

View File

@ -22,7 +22,7 @@ export default function App() {
const { data: auth, loading: loadingAuth, newSession } = useAuth()
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 ? (
<div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
) : (
@ -136,24 +136,21 @@ function Header({
auth: AuthResponse
newSession: () => Promise<void>
}) {
const [loc, setLocation] = useLocation()
const [loc] = useLocation()
return (
<>
<div className="flex justify-between items-center mb-9 md:mb-12">
<div className="flex gap-3">
<TailscaleIcon
className="cursor-pointer"
onClick={() => setLocation("/")}
/>
<div className="inline text-gray-800 text-lg font-medium leading-snug">
<div className="flex flex-wrap gap-4 justify-between items-center mb-9 md:mb-12">
<Link to="/" className="flex gap-3 overflow-hidden">
<TailscaleIcon />
<div className="inline text-gray-800 text-lg font-medium leading-snug truncate">
{node.DomainName}
</div>
</div>
</Link>
<LoginToggle node={node} auth={auth} newSession={newSession} />
</div>
{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}
</Link>
)}

View File

@ -58,38 +58,38 @@ export default function ExitNodeSelector({
)
return (
<Popover
open={disabled ? false : open}
onOpenChange={setOpen}
side="bottom"
sideOffset={5}
align="start"
alignOffset={8}
content={
<ExitNodeSelectorInner
node={node}
selected={selected}
onSelect={handleSelect}
/>
}
asChild
<div
className={cx(
"rounded-md",
{
"bg-red-600": offline,
},
className
)}
>
<div
className={cx(
"rounded-md",
{
"bg-red-600": offline,
},
className
)}
className={cx("p-1.5 rounded-md border flex items-stretch gap-1.5", {
"border-gray-200": none,
"bg-yellow-300 border-yellow-300": advertising && !offline,
"bg-blue-500 border-blue-500": using && !offline,
"bg-red-500 border-red-500": offline,
})}
>
<div
className={cx("p-1.5 rounded-md border flex items-stretch gap-1.5", {
"border-gray-200": none,
"bg-yellow-300 border-yellow-300": advertising && !offline,
"bg-blue-500 border-blue-500": using && !offline,
"bg-red-500 border-red-500": offline,
})}
<Popover
open={disabled ? false : open}
onOpenChange={setOpen}
className="overflow-hidden"
side="bottom"
sideOffset={0}
align="start"
content={
<ExitNodeSelectorInner
node={node}
selected={selected}
onSelect={handleSelect}
/>
}
asChild
>
<button
className={cx("flex-1 px-2 py-1.5 rounded-[1px]", {
@ -108,7 +108,7 @@ export default function ExitNodeSelector({
<p
className={cx(
"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"}
@ -138,32 +138,31 @@ export default function ExitNodeSelector({
)}
</div>
</button>
{!disabled && (advertising || using) && (
<button
className={cx("px-3 py-2 rounded-sm text-white", {
"bg-yellow-200": advertising && !offline,
"bg-blue-400": using && !offline,
"bg-red-400": offline,
})}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleSelect(noExitNode)
}}
>
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>
</Popover>
{!disabled && (advertising || using) && (
<button
className={cx("px-3 py-2 rounded-sm text-white", {
"hover:bg-yellow-200": advertising && !offline,
"hover:bg-blue-400": using && !offline,
"hover:bg-red-400": offline,
})}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleSelect(noExitNode)
}}
>
Disable
</button>
)}
</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 (
<div className="w-[calc(var(--radix-popover-trigger-width)-16px)] pb-1 rounded-lg shadow">
<div className="w-[var(--radix-popover-trigger-width)]">
<SearchInput
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
autoCorrect="off"
autoComplete="off"
@ -224,7 +224,7 @@ function ExitNodeSelectorInner({
{/* TODO(sonia): use loading spinner when loading useExitNodes */}
<div
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 ? (
exitNodes.map(

View File

@ -57,7 +57,7 @@ export default function LoginToggle({
) : (
<div
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-gray-300": open,

View File

@ -33,7 +33,7 @@ export default function HomeView({
<h2 className="mb-3">This device</h2>
<div className="-mx-5 card mb-9">
<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">
<Machine />
</div>
@ -49,9 +49,10 @@ export default function HomeView({
{node.Status === "Running" ? "Connected" : "Offline"}
</p>
</div>
</div>
</Link>
<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}
v6Address={node.IPv6}
shortDomain={node.DeviceName}
@ -150,7 +151,7 @@ function SettingsCard({
return (
<button
className={cx("-mx-5 card cursor-pointer", className)}
className={cx("-mx-5 card cursor-pointer", { "pb-4": footer }, className)}
onClick={() => setLocation(link)}
>
<div className="flex justify-between items-center">

View File

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

View File

@ -8,6 +8,8 @@ import { ReactComponent as Plus } from "src/assets/icons/plus.svg"
import * as Control from "src/components/control-components"
import { NodeData, NodeUpdaters } from "src/hooks/node-data"
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"
export default function SubnetRouterView({
@ -50,7 +52,7 @@ export default function SubnetRouterView({
</p>
{!readonly &&
(inputOpen ? (
<div className="-mx-5 card shadow">
<div className="-mx-5 card !border-0 shadow-popover">
<p className="font-medium leading-snug mb-3">
Advertise new routes
</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">
Not advertising any routes
</div>
<Card empty>
<EmptyState description="Not advertising any routes" />
</Card>
)}
</div>
</>

View File

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

View File

@ -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>
)
}

View File

@ -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
}

View File

@ -17,10 +17,10 @@ const SearchInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
const { className, inputClassName, ...rest } = props
return (
<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
type="text"
className={cx("input px-8", inputClassName)}
className={cx("input pl-9 pr-8", inputClassName)}
ref={ref}
{...rest}
/>