Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 2 additions & 34 deletions site/src/components/CopyButton/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import IconButton from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import Tooltip from "@material-ui/core/Tooltip"
import Check from "@material-ui/icons/Check"
import React, { useState } from "react"
import { useClipboard } from "hooks/useClipboard"
import { combineClasses } from "../../util/combineClasses"
import { FileCopyIcon } from "../Icons/FileCopyIcon"

Expand Down Expand Up @@ -30,39 +30,7 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
tooltipTitle = Language.tooltipTitle,
}) => {
const styles = useStyles()
const [isCopied, setIsCopied] = useState<boolean>(false)

const copyToClipboard = async (): Promise<void> => {
try {
await window.navigator.clipboard.writeText(text)
setIsCopied(true)
window.setTimeout(() => {
setIsCopied(false)
}, 1000)
} catch (err) {
const input = document.createElement("input")
input.value = text
document.body.appendChild(input)
input.focus()
input.select()
const result = document.execCommand("copy")
document.body.removeChild(input)
if (result) {
setIsCopied(true)
window.setTimeout(() => {
setIsCopied(false)
}, 1000)
} else {
const wrappedErr = new Error(
"copyToClipboard: failed to copy text to clipboard",
)
if (err instanceof Error) {
wrappedErr.stack = err.stack
}
console.error(wrappedErr)
}
}
}
const { isCopied, copy: copyToClipboard } = useClipboard(text)

return (
<Tooltip title={tooltipTitle} placement="top">
Expand Down
39 changes: 39 additions & 0 deletions site/src/components/CopyableValue/CopyableValue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { makeStyles } from "@material-ui/core/styles"
import Tooltip from "@material-ui/core/Tooltip"
import { useClickable } from "hooks/useClickable"
import { useClipboard } from "hooks/useClipboard"
import React, { HTMLProps } from "react"
import { combineClasses } from "util/combineClasses"

interface CopyableValueProps extends HTMLProps<HTMLDivElement> {
value: string
}

export const CopyableValue: React.FC<CopyableValueProps> = ({
value,
className,
...props
}) => {
const { isCopied, copy } = useClipboard(value)
const clickableProps = useClickable(copy)
const styles = useStyles()

return (
<Tooltip
title={isCopied ? "Copied!" : "Click to copy"}
placement="bottom-start"
>
<span
{...props}
{...clickableProps}
className={combineClasses([styles.value, className])}
/>
</Tooltip>
)
}

const useStyles = makeStyles(() => ({
value: {
cursor: "pointer",
},
}))
7 changes: 5 additions & 2 deletions site/src/components/PageHeader/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ export const PageHeader: React.FC<React.PropsWithChildren<PageHeaderProps>> = ({
const styles = useStyles({})

return (
<div className={combineClasses([styles.root, className])}>
<header
className={combineClasses([styles.root, className])}
data-testid="header"
>
<hgroup>{children}</hgroup>
{actions && (
<Stack direction="row" className={styles.actions}>
{actions}
</Stack>
)}
</div>
</header>
)
}

Expand Down
114 changes: 114 additions & 0 deletions site/src/components/Resources/AgentLatency.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useRef, useState, FC } from "react"
import { makeStyles, Theme, useTheme } from "@material-ui/core/styles"
import {
HelpTooltipText,
HelpPopover,
HelpTooltipTitle,
} from "components/Tooltips/HelpTooltip"
import { Stack } from "components/Stack/Stack"
import { WorkspaceAgent, DERPRegion } from "api/typesGenerated"

const getDisplayLatency = (theme: Theme, agent: WorkspaceAgent) => {
// Find the right latency to display
const latencyValues = Object.values(agent.latency ?? {})
const latency =
latencyValues.find((derp) => derp.preferred) ??
// Accessing an array index can return undefined as well
// for some reason TS does not handle that
(latencyValues[0] as DERPRegion | undefined)

if (!latency) {
return undefined
}

// Get the color
let color = theme.palette.success.light
if (latency.latency_ms >= 150 && latency.latency_ms < 300) {
color = theme.palette.warning.light
} else if (latency.latency_ms >= 300) {
color = theme.palette.error.light
}

return {
...latency,
color,
}
}

export const AgentLatency: FC<{ agent: WorkspaceAgent }> = ({ agent }) => {
const theme: Theme = useTheme()
const anchorRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
const id = isOpen ? "latency-popover" : undefined
const latency = getDisplayLatency(theme, agent)
const styles = useStyles()

if (!latency || !agent.latency) {
return null
}

return (
<>
<span
role="presentation"
aria-label="latency"
ref={anchorRef}
onMouseEnter={() => setIsOpen(true)}
className={styles.trigger}
style={{ color: latency.color }}
>
{Math.round(Math.round(latency.latency_ms))}ms
</span>
<HelpPopover
id={id}
open={isOpen}
anchorEl={anchorRef.current}
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
>
<HelpTooltipTitle>Latency</HelpTooltipTitle>
<HelpTooltipText>
Latency from relay servers, used when connections cannot connect
peer-to-peer. Star indicates the preferred relay.
</HelpTooltipText>

<HelpTooltipText>
<Stack direction="column" spacing={1} className={styles.regions}>
{Object.keys(agent.latency).map((regionName) => {
if (!agent.latency) {
throw new Error("No latency found on agent")
}

const region = agent.latency[regionName]

return (
<Stack
direction="row"
key={regionName}
spacing={0.5}
justifyContent="space-between"
className={region.preferred ? styles.preferred : undefined}
>
<strong>{regionName}</strong>
{Math.round(region.latency_ms)}ms
</Stack>
)
})}
</Stack>
</HelpTooltipText>
</HelpPopover>
</>
)
}

const useStyles = makeStyles((theme) => ({
trigger: {
cursor: "pointer",
},
regions: {
marginTop: theme.spacing(2),
},
preferred: {
color: theme.palette.text.primary,
},
}))
100 changes: 100 additions & 0 deletions site/src/components/Resources/AgentStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Tooltip from "@material-ui/core/Tooltip"
import { makeStyles } from "@material-ui/core/styles"
import { combineClasses } from "util/combineClasses"
import { WorkspaceAgent } from "api/typesGenerated"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
import { useTranslation } from "react-i18next"

const ConnectedStatus: React.FC = () => {
const styles = useStyles()
const { t } = useTranslation("workspacePage")

return (
<Tooltip title={t("agentStatus.connected")}>
<div
role="status"
aria-label={t("agentStatus.connected")}
className={combineClasses([styles.status, styles.connected])}
/>
</Tooltip>
)
}

const DisconnectedStatus: React.FC = () => {
const styles = useStyles()
const { t } = useTranslation("workspacePage")

return (
<Tooltip title={t("agentStatus.disconnected")}>
<div
role="status"
aria-label={t("agentStatus.disconnected")}
className={combineClasses([styles.status, styles.disconnected])}
/>
</Tooltip>
)
}

const ConnectingStatus: React.FC = () => {
const styles = useStyles()
const { t } = useTranslation("workspacePage")

return (
<Tooltip title={t("agentStatus.connecting")}>
<div
role="status"
aria-label={t("agentStatus.connecting")}
className={combineClasses([styles.status, styles.connecting])}
/>
</Tooltip>
)
}

export const AgentStatus: React.FC<{ agent: WorkspaceAgent }> = ({ agent }) => {
return (
<ChooseOne>
<Cond condition={agent.status === "connected"}>
<ConnectedStatus />
</Cond>
<Cond condition={agent.status === "disconnected"}>
<DisconnectedStatus />
</Cond>
<Cond>
<ConnectingStatus />
</Cond>
</ChooseOne>
)
}

const useStyles = makeStyles((theme) => ({
status: {
width: theme.spacing(1),
height: theme.spacing(1),
borderRadius: "100%",
},

connected: {
backgroundColor: theme.palette.success.light,
},

disconnected: {
backgroundColor: theme.palette.text.secondary,
},

"@keyframes pulse": {
"0%": {
opacity: 0.25,
},
"50%": {
opacity: 1,
},
"100%": {
opacity: 0.25,
},
},

connecting: {
backgroundColor: theme.palette.info.light,
animation: "$pulse 1s ease-in-out forwards infinite",
},
}))
61 changes: 61 additions & 0 deletions site/src/components/Resources/AgentVersion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useRef, useState, FC } from "react"
import { makeStyles } from "@material-ui/core/styles"
import {
HelpTooltipText,
HelpPopover,
HelpTooltipTitle,
} from "components/Tooltips/HelpTooltip"
import { WorkspaceAgent } from "api/typesGenerated"
import { getDisplayVersionStatus } from "util/workspace"

export const AgentVersion: FC<{
agent: WorkspaceAgent
serverVersion: string
}> = ({ agent, serverVersion }) => {
const styles = useStyles()
const anchorRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
const id = isOpen ? "version-outdated-popover" : undefined
const { displayVersion, outdated } = getDisplayVersionStatus(
agent.version,
serverVersion,
)

if (!outdated) {
return <span>{displayVersion}</span>
}

return (
<>
<span
role="presentation"
aria-label="latency"
ref={anchorRef}
onMouseEnter={() => setIsOpen(true)}
className={styles.trigger}
>
Agent Outdated
</span>
<HelpPopover
id={id}
open={isOpen}
anchorEl={anchorRef.current}
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
>
<HelpTooltipTitle>Agent Outdated</HelpTooltipTitle>
<HelpTooltipText>
This agent is an older version than the Coder server. This can happen
after you update Coder with running workspaces. To fix this, you can
stop and start the workspace.
</HelpTooltipText>
</HelpPopover>
</>
)
}

const useStyles = makeStyles(() => ({
trigger: {
cursor: "pointer",
},
}))
Loading