Skip to content

feat: Redesign resources table #4600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 18, 2022
Prev Previous commit
Next Next commit
Add collapsable metadata
  • Loading branch information
BrunoQuaresma committed Oct 17, 2022
commit d71f37b1a221e552e013dd46dd07fc1913b45ea0
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
5 changes: 5 additions & 0 deletions site/src/components/Resources/ResourceCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ BunchOfMetadata.args = {
sensitive: false,
},
{ key: "volume", value: "/home/coder", sensitive: false },
{
key: "secret",
value: "3XqfNW0b1bvsGsqud8O6OW6VabH3fwzI",
sensitive: true,
},
],
},
}
132 changes: 89 additions & 43 deletions site/src/components/Resources/ResourceCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { makeStyles } from "@material-ui/core/styles"
import { Skeleton } from "@material-ui/lab"
import { PortForwardButton } from "components/PortForwardButton/PortForwardButton"
import { FC } from "react"
import { FC, useState } from "react"
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
import { AppLink } from "../AppLink/AppLink"
import { SSHButton } from "../SSHButton/SSHButton"
Expand All @@ -11,6 +11,13 @@ import { ResourceAvatar } from "./ResourceAvatar"
import { SensitiveValue } from "./SensitiveValue"
import { AgentLatency } from "./AgentLatency"
import { AgentVersion } from "./AgentVersion"
import {
OpenDropdown,
CloseDropdown,
} from "components/DropdownArrows/DropdownArrows"
import IconButton from "@material-ui/core/IconButton"
import Tooltip from "@material-ui/core/Tooltip"
import { Maybe } from "components/Conditionals/Maybe"

export interface ResourceCardProps {
resource: WorkspaceResource
Expand All @@ -29,43 +36,76 @@ export const ResourceCard: FC<ResourceCardProps> = ({
hideSSHButton,
serverVersion,
}) => {
const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] =
useState(false)
const styles = useStyles()

const metadataToDisplay =
// Type is already displayed in the header
resource.metadata?.filter((data) => data.key !== "type") ?? []
const visibleMetadata = shouldDisplayAllMetadata
? metadataToDisplay
: metadataToDisplay.slice(0, 4)

return (
<div key={resource.id} className={styles.resourceCard}>
<Stack
direction="row"
alignItems="center"
alignItems="flex-start"
className={styles.resourceCardHeader}
spacing={10}
>
<div>
<ResourceAvatar resource={resource} />
</div>
<div className={styles.resourceHeader}>
<div className={styles.resourceHeaderLabel}>{resource.type}</div>
<div>{resource.name}</div>
</div>
</Stack>
<Stack
direction="row"
alignItems="center"
className={styles.resourceCardProfile}
>
<div>
<ResourceAvatar resource={resource} />
</div>
<div className={styles.metadata}>
<div className={styles.metadataLabel}>{resource.type}</div>
<div className={styles.metadataValue}>{resource.name}</div>
</div>
</Stack>

<Stack
direction="row"
alignItems="baseline"
wrap="wrap"
className={styles.resourceMetadata}
>
{metadataToDisplay.map((data) => (
<div key={data.key} className={styles.resourceData}>
<span className={styles.resourceDataLabel}>{data.key}:</span>
{data.sensitive ? (
<SensitiveValue value={data.value} />
) : (
<span>{data.value}</span>
)}
<Stack alignItems="flex-start" direction="row" spacing={5}>
<div className={styles.metadataHeader}>
{visibleMetadata.map((meta) => {
return (
<div className={styles.metadata} key={meta.key}>
<div className={styles.metadataLabel}>{meta.key}</div>
<div className={styles.metadataValue}>
{meta.sensitive ? (
<SensitiveValue value={meta.value} />
) : (
meta.value
)}
</div>
</div>
)
})}
</div>
))}

<Maybe condition={metadataToDisplay.length > 4}>
<Tooltip
title={
shouldDisplayAllMetadata ? "Hide metadata" : "Show all metadata"
}
>
<IconButton
onClick={() => {
setShouldDisplayAllMetadata((value) => !value)
}}
>
{shouldDisplayAllMetadata ? (
<CloseDropdown margin={false} />
) : (
<OpenDropdown margin={false} />
)}
</IconButton>
</Tooltip>
</Maybe>
</Stack>
</Stack>

<div>
Expand Down Expand Up @@ -132,14 +172,15 @@ export const ResourceCard: FC<ResourceCardProps> = ({
workspaceName={workspace.name}
agentName={agent.name}
health={app.health}
appSharingLevel={app.sharing_level}
/>
))}
</>
)}
{showApps && agent.status === "connecting" && (
<>
<Skeleton width={80} height={60} />
<Skeleton width={120} height={60} />
<Skeleton width={80} height={36} variant="rect" />
<Skeleton width={120} height={36} variant="rect" />
</>
)}
</Stack>
Expand All @@ -158,35 +199,40 @@ const useStyles = makeStyles((theme) => ({
border: `1px solid ${theme.palette.divider}`,
},

resourceCardProfile: {
flexShrink: 0,
width: "fit-content",
},

resourceCardHeader: {
padding: theme.spacing(3, 4),
borderBottom: `1px solid ${theme.palette.divider}`,
},

resourceMetadata: {
padding: theme.spacing(2, 4),
borderBottom: `1px solid ${theme.palette.divider}`,
gap: theme.spacing(0.5, 2),
metadataHeader: {
display: "grid",
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
gap: theme.spacing(5),
rowGap: theme.spacing(3),
},

resourceHeader: {
metadata: {
fontSize: 16,
},

resourceHeaderLabel: {
metadataLabel: {
fontSize: 12,
color: theme.palette.text.secondary,
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
},

resourceData: {
fontSize: 12,
flexShrink: 0,
},

resourceDataLabel: {
fontSize: 12,
color: theme.palette.text.secondary,
marginRight: theme.spacing(0.75),
metadataValue: {
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
userSelect: "all",
},

agentRow: {
Expand Down
1 change: 1 addition & 0 deletions site/src/components/Resources/Resources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
workspaceName={workspace.name}
agentName={agent.name}
health={app.health}
appSharingLevel={app.sharing_level}
/>
))}
</>
Expand Down
12 changes: 10 additions & 2 deletions site/src/components/Resources/SensitiveValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const SensitiveValue: React.FC<{ value: string }> = ({ value }) => {

return (
<div className={styles.sensitiveValue}>
{displayValue}
<div className={styles.value}>{displayValue}</div>
<Tooltip title={buttonLabel}>
<IconButton
className={styles.button}
Expand All @@ -41,13 +41,21 @@ export const SensitiveValue: React.FC<{ value: string }> = ({ value }) => {
}

const useStyles = makeStyles((theme) => ({
value: {
// 22px is the button width
width: "calc(100% - 22px)",
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
},

sensitiveValue: {
display: "flex",
alignItems: "center",
gap: theme.spacing(0.5),
},

button: {
marginLeft: theme.spacing(0.5),
color: "inherit",

"& .MuiSvgIcon-root": {
Expand Down
44 changes: 44 additions & 0 deletions site/src/hooks/useClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState } from "react"

export const useClipboard = (
text: string,
): { isCopied: boolean; copy: () => Promise<void> } => {
const [isCopied, setIsCopied] = useState<boolean>(false)

const copy = 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)
}
}
}

return {
isCopied,
copy,
}
}