From 014ae9829742383dd63c146b8ea26af95dac3532 Mon Sep 17 00:00:00 2001 From: Sonia Appasamy Date: Wed, 6 Dec 2023 16:11:00 -0500 Subject: [PATCH] client/web: style tweaks Style changes made in live pairing session. Updates #10261 Co-authored-by: Will Norris Co-authored-by: Alessandro Mingione Signed-off-by: Sonia Appasamy --- .../web/src/components/address-copy-card.tsx | 4 +- client/web/src/components/app.tsx | 19 ++- .../web/src/components/exit-node-selector.tsx | 114 +++++++++--------- client/web/src/components/login-toggle.tsx | 2 +- client/web/src/components/views/home-view.tsx | 9 +- client/web/src/components/views/ssh-view.tsx | 35 +++--- .../components/views/subnet-router-view.tsx | 10 +- client/web/src/hooks/exit-nodes.ts | 2 +- client/web/src/ui/card.tsx | 40 ++++++ client/web/src/ui/empty-state.tsx | 44 +++++++ client/web/src/ui/search-input.tsx | 4 +- 11 files changed, 186 insertions(+), 97 deletions(-) create mode 100644 client/web/src/ui/card.tsx create mode 100644 client/web/src/ui/empty-state.tsx diff --git a/client/web/src/components/address-copy-card.tsx b/client/web/src/components/address-copy-card.tsx index 3ed582997..d24031f34 100644 --- a/client/web/src/components/address-copy-card.tsx +++ b/client/web/src/components/address-copy-card.tsx @@ -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({ - {!disabled && (advertising || using) && ( - - )} - - {offline && ( -

- The selected exit node is currently offline. Your internet traffic - is blocked until you disable the exit node or select a different - one. -

+ + {!disabled && (advertising || using) && ( + )} - + {offline && ( +

+ The selected exit node is currently offline. Your internet traffic is + blocked until you disable the exit node or select a different one. +

+ )} + ) } @@ -205,10 +204,11 @@ function ExitNodeSelectorInner({ ) return ( -
+
{hasNodes ? ( exitNodes.map( diff --git a/client/web/src/components/login-toggle.tsx b/client/web/src/components/login-toggle.tsx index 53b52bc98..fe0811f14 100644 --- a/client/web/src/components/login-toggle.tsx +++ b/client/web/src/components/login-toggle.tsx @@ -57,7 +57,7 @@ export default function LoginToggle({ ) : (
This device
-
+
@@ -49,9 +49,10 @@ export default function HomeView({ {node.Status === "Running" ? "Connected" : "Offline"}

-
+ setLocation(link)} >
diff --git a/client/web/src/components/views/ssh-view.tsx b/client/web/src/components/views/ssh-view.tsx index ad756a79b..0ec0946f2 100644 --- a/client/web/src/components/views/ssh-view.tsx +++ b/client/web/src/components/views/ssh-view.tsx @@ -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 →

-
- - nodeUpdaters.patchPrefs({ - RunSSHSet: true, - RunSSH: !node.RunningSSHServer, - }) - } - disabled={readonly} - /> -
- Run Tailscale SSH server -
-
+ + + Remember to make sure that the{" "} diff --git a/client/web/src/components/views/subnet-router-view.tsx b/client/web/src/components/views/subnet-router-view.tsx index fa20bc679..b1b81f953 100644 --- a/client/web/src/components/views/subnet-router-view.tsx +++ b/client/web/src/components/views/subnet-router-view.tsx @@ -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({

{!readonly && (inputOpen ? ( -
+

Advertise new routes

@@ -150,9 +152,9 @@ export default function SubnetRouterView({ )} ) : ( -
- Not advertising any routes -
+ + + )}
diff --git a/client/web/src/hooks/exit-nodes.ts b/client/web/src/hooks/exit-nodes.ts index f9013dced..7d9fa4cf7 100644 --- a/client/web/src/hooks/exit-nodes.ts +++ b/client/web/src/hooks/exit-nodes.ts @@ -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, } diff --git a/client/web/src/ui/card.tsx b/client/web/src/ui/card.tsx new file mode 100644 index 000000000..4e17c3df6 --- /dev/null +++ b/client/web/src/ui/card.tsx @@ -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. + * + * {content} + * {content} + * + * + */ +export default function Card(props: Props) { + const { children, className, elevated, empty, noPadding } = props + return ( +
+ {children} +
+ ) +} diff --git a/client/web/src/ui/empty-state.tsx b/client/web/src/ui/empty-state.tsx new file mode 100644 index 000000000..6ac7fd4fa --- /dev/null +++ b/client/web/src/ui/empty-state.tsx @@ -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 ( +
+ {icon &&
{iconComponent}
} + {title && ( +

{title}

+ )} +
+ {description} +
+ {action &&
{action}
} +
+ ) +} + +function getIcon(icon: React.ReactElement | undefined, iconColor: string) { + return icon ? cloneElement(icon, { className: iconColor }) : null +} diff --git a/client/web/src/ui/search-input.tsx b/client/web/src/ui/search-input.tsx index 4d919e07d..80579c2cf 100644 --- a/client/web/src/ui/search-input.tsx +++ b/client/web/src/ui/search-input.tsx @@ -17,10 +17,10 @@ const SearchInput = forwardRef((props, ref) => { const { className, inputClassName, ...rest } = props return (
- +