mirror of https://github.com/tailscale/tailscale/
client/web: add readonly/manage toggle
Updates tailscale/corp#14335 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>kristoffer/editable-tailnet-displayname
parent
c54d680682
commit
86c8ab7502
@ -1,75 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import { AuthResponse, AuthType } 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,
|
|
||||||
newSession,
|
|
||||||
}: {
|
|
||||||
data: NodeData
|
|
||||||
auth?: AuthResponse
|
|
||||||
newSession: () => Promise<void>
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="pb-52 mx-auto">
|
|
||||||
<TailscaleLogo />
|
|
||||||
</div>
|
|
||||||
<div className="w-full p-4 bg-stone-50 rounded-3xl border border-gray-200 flex flex-col gap-4">
|
|
||||||
<div className="flex gap-2.5">
|
|
||||||
<ProfilePic url={data.Profile.ProfilePicURL} />
|
|
||||||
<div className="font-medium">
|
|
||||||
<div className="text-neutral-500 text-xs uppercase tracking-wide">
|
|
||||||
Managed by
|
|
||||||
</div>
|
|
||||||
<div className="text-neutral-800 text-sm leading-tight">
|
|
||||||
{/* TODO(sonia): support tagged node profile view more eloquently */}
|
|
||||||
{data.Profile.LoginName}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-5 py-4 bg-white rounded-lg border border-gray-200 justify-between items-center flex">
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<ConnectedDeviceIcon />
|
|
||||||
<div className="text-neutral-800">
|
|
||||||
<div className="text-lg font-medium leading-[25.20px]">
|
|
||||||
{data.DeviceName}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm leading-tight">{data.IP}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{auth?.authNeeded == AuthType.tailscale ? (
|
|
||||||
<button className="button button-blue ml-6" onClick={newSession}>
|
|
||||||
Access
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
window.location.hostname != data.IP && (
|
|
||||||
// TODO: check connectivity to tailscale IP
|
|
||||||
<button
|
|
||||||
className="button button-blue ml-6"
|
|
||||||
onClick={() => {
|
|
||||||
window.location.href = `http://${data.IP}:5252/?check=now`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Manage
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_15367_14595)">
|
||||||
|
<path d="M0.625 8C0.625 8 3.125 3 7.5 3C11.875 3 14.375 8 14.375 8C14.375 8 11.875 13 7.5 13C3.125 13 0.625 8 0.625 8Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M7.5 9.875C8.53553 9.875 9.375 9.03553 9.375 8C9.375 6.96447 8.53553 6.125 7.5 6.125C6.46447 6.125 5.625 6.96447 5.625 8C5.625 9.03553 6.46447 9.875 7.5 9.875Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_15367_14595">
|
||||||
|
<rect width="15" height="15" fill="white" transform="translate(0 0.5)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 738 B |
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.5 13.625V12.375C12.5 11.712 12.2366 11.0761 11.7678 10.6072C11.2989 10.1384 10.663 9.875 10 9.875H5C4.33696 9.875 3.70107 10.1384 3.23223 10.6072C2.76339 11.0761 2.5 11.712 2.5 12.375V13.625" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M7.5 7.375C8.88071 7.375 10 6.25571 10 4.875C10 3.49429 8.88071 2.375 7.5 2.375C6.11929 2.375 5 3.49429 5 4.875C5 6.25571 6.11929 7.375 7.5 7.375Z" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 635 B |
@ -0,0 +1,106 @@
|
|||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
import cx from "classnames"
|
||||||
|
import React, { ReactNode } from "react"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
content: ReactNode
|
||||||
|
children: ReactNode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* asChild renders the trigger element without wrapping it in a button. Use
|
||||||
|
* this when you want to use a `button` element as the trigger.
|
||||||
|
*/
|
||||||
|
asChild?: boolean
|
||||||
|
/**
|
||||||
|
* side is the side of the direction from the target element to render the
|
||||||
|
* popover.
|
||||||
|
*/
|
||||||
|
side?: "top" | "bottom" | "left" | "right"
|
||||||
|
/**
|
||||||
|
* sideOffset is how far from a give side to render the popover.
|
||||||
|
*/
|
||||||
|
sideOffset?: number
|
||||||
|
/**
|
||||||
|
* align is how to align the popover with the target element.
|
||||||
|
*/
|
||||||
|
align?: "start" | "center" | "end"
|
||||||
|
/**
|
||||||
|
* alignOffset is how far off of the alignment point to render the popover.
|
||||||
|
*/
|
||||||
|
alignOffset?: number
|
||||||
|
|
||||||
|
open?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popover is a UI component that allows rendering unique controls in a floating
|
||||||
|
* popover, attached to a trigger element. It appears on click and manages focus
|
||||||
|
* on its own behalf.
|
||||||
|
*
|
||||||
|
* To use the Popover, pass the content as children, and give it a `trigger`:
|
||||||
|
*
|
||||||
|
* <Popover trigger={<span>Open popover</span>}>
|
||||||
|
* <p>Hello world!</p>
|
||||||
|
* </Popover>
|
||||||
|
*
|
||||||
|
* By default, the toggle is wrapped in an accessible <button> tag. You can
|
||||||
|
* customize by providing your own button and using the `asChild` prop.
|
||||||
|
*
|
||||||
|
* <Popover trigger={<Button>Hello</Button>} asChild>
|
||||||
|
* <p>Hello world!</p>
|
||||||
|
* </Popover>
|
||||||
|
*
|
||||||
|
* The former style is recommended whenever possible.
|
||||||
|
*/
|
||||||
|
export default function Popover(props: Props) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
content,
|
||||||
|
side,
|
||||||
|
sideOffset,
|
||||||
|
align,
|
||||||
|
alignOffset,
|
||||||
|
asChild,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<PopoverPrimitive.Trigger asChild={asChild}>
|
||||||
|
{children}
|
||||||
|
</PopoverPrimitive.Trigger>
|
||||||
|
<PortalContainerContext.Consumer>
|
||||||
|
{(portalContainer) => (
|
||||||
|
<PopoverPrimitive.Portal container={portalContainer}>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
className={cx(
|
||||||
|
"origin-radix-popover shadow-popover bg-white rounded-md z-50",
|
||||||
|
"state-open:animate-scale-in state-closed:animate-scale-out",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
side={side}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
align={align}
|
||||||
|
alignOffset={alignOffset}
|
||||||
|
collisionPadding={12}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</PopoverPrimitive.Content>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)}
|
||||||
|
</PortalContainerContext.Consumer>
|
||||||
|
</PopoverPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Popover.defaultProps = {
|
||||||
|
sideOffset: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PortalContainerContext = React.createContext<HTMLElement | undefined>(
|
||||||
|
undefined
|
||||||
|
)
|
Loading…
Reference in New Issue