Skip to content

Commit d71f37b

Browse files
committed
Add collapsable metadata
1 parent 446b7bd commit d71f37b

File tree

6 files changed

+151
-79
lines changed

6 files changed

+151
-79
lines changed

site/src/components/CopyButton/CopyButton.tsx

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import IconButton from "@material-ui/core/Button"
22
import { makeStyles } from "@material-ui/core/styles"
33
import Tooltip from "@material-ui/core/Tooltip"
44
import Check from "@material-ui/icons/Check"
5-
import React, { useState } from "react"
5+
import { useClipboard } from "hooks/useClipboard"
66
import { combineClasses } from "../../util/combineClasses"
77
import { FileCopyIcon } from "../Icons/FileCopyIcon"
88

@@ -30,39 +30,7 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
3030
tooltipTitle = Language.tooltipTitle,
3131
}) => {
3232
const styles = useStyles()
33-
const [isCopied, setIsCopied] = useState<boolean>(false)
34-
35-
const copyToClipboard = async (): Promise<void> => {
36-
try {
37-
await window.navigator.clipboard.writeText(text)
38-
setIsCopied(true)
39-
window.setTimeout(() => {
40-
setIsCopied(false)
41-
}, 1000)
42-
} catch (err) {
43-
const input = document.createElement("input")
44-
input.value = text
45-
document.body.appendChild(input)
46-
input.focus()
47-
input.select()
48-
const result = document.execCommand("copy")
49-
document.body.removeChild(input)
50-
if (result) {
51-
setIsCopied(true)
52-
window.setTimeout(() => {
53-
setIsCopied(false)
54-
}, 1000)
55-
} else {
56-
const wrappedErr = new Error(
57-
"copyToClipboard: failed to copy text to clipboard",
58-
)
59-
if (err instanceof Error) {
60-
wrappedErr.stack = err.stack
61-
}
62-
console.error(wrappedErr)
63-
}
64-
}
65-
}
33+
const { isCopied, copy: copyToClipboard } = useClipboard(text)
6634

6735
return (
6836
<Tooltip title={tooltipTitle} placement="top">

site/src/components/Resources/ResourceCard.stories.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ BunchOfMetadata.args = {
7171
sensitive: false,
7272
},
7373
{ key: "volume", value: "/home/coder", sensitive: false },
74+
{
75+
key: "secret",
76+
value: "3XqfNW0b1bvsGsqud8O6OW6VabH3fwzI",
77+
sensitive: true,
78+
},
7479
],
7580
},
7681
}

site/src/components/Resources/ResourceCard.tsx

Lines changed: 89 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { makeStyles } from "@material-ui/core/styles"
22
import { Skeleton } from "@material-ui/lab"
33
import { PortForwardButton } from "components/PortForwardButton/PortForwardButton"
4-
import { FC } from "react"
4+
import { FC, useState } from "react"
55
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
66
import { AppLink } from "../AppLink/AppLink"
77
import { SSHButton } from "../SSHButton/SSHButton"
@@ -11,6 +11,13 @@ import { ResourceAvatar } from "./ResourceAvatar"
1111
import { SensitiveValue } from "./SensitiveValue"
1212
import { AgentLatency } from "./AgentLatency"
1313
import { AgentVersion } from "./AgentVersion"
14+
import {
15+
OpenDropdown,
16+
CloseDropdown,
17+
} from "components/DropdownArrows/DropdownArrows"
18+
import IconButton from "@material-ui/core/IconButton"
19+
import Tooltip from "@material-ui/core/Tooltip"
20+
import { Maybe } from "components/Conditionals/Maybe"
1421

1522
export interface ResourceCardProps {
1623
resource: WorkspaceResource
@@ -29,43 +36,76 @@ export const ResourceCard: FC<ResourceCardProps> = ({
2936
hideSSHButton,
3037
serverVersion,
3138
}) => {
39+
const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] =
40+
useState(false)
3241
const styles = useStyles()
33-
3442
const metadataToDisplay =
43+
// Type is already displayed in the header
3544
resource.metadata?.filter((data) => data.key !== "type") ?? []
45+
const visibleMetadata = shouldDisplayAllMetadata
46+
? metadataToDisplay
47+
: metadataToDisplay.slice(0, 4)
3648

3749
return (
3850
<div key={resource.id} className={styles.resourceCard}>
3951
<Stack
4052
direction="row"
41-
alignItems="center"
53+
alignItems="flex-start"
4254
className={styles.resourceCardHeader}
55+
spacing={10}
4356
>
44-
<div>
45-
<ResourceAvatar resource={resource} />
46-
</div>
47-
<div className={styles.resourceHeader}>
48-
<div className={styles.resourceHeaderLabel}>{resource.type}</div>
49-
<div>{resource.name}</div>
50-
</div>
51-
</Stack>
57+
<Stack
58+
direction="row"
59+
alignItems="center"
60+
className={styles.resourceCardProfile}
61+
>
62+
<div>
63+
<ResourceAvatar resource={resource} />
64+
</div>
65+
<div className={styles.metadata}>
66+
<div className={styles.metadataLabel}>{resource.type}</div>
67+
<div className={styles.metadataValue}>{resource.name}</div>
68+
</div>
69+
</Stack>
5270

53-
<Stack
54-
direction="row"
55-
alignItems="baseline"
56-
wrap="wrap"
57-
className={styles.resourceMetadata}
58-
>
59-
{metadataToDisplay.map((data) => (
60-
<div key={data.key} className={styles.resourceData}>
61-
<span className={styles.resourceDataLabel}>{data.key}:</span>
62-
{data.sensitive ? (
63-
<SensitiveValue value={data.value} />
64-
) : (
65-
<span>{data.value}</span>
66-
)}
71+
<Stack alignItems="flex-start" direction="row" spacing={5}>
72+
<div className={styles.metadataHeader}>
73+
{visibleMetadata.map((meta) => {
74+
return (
75+
<div className={styles.metadata} key={meta.key}>
76+
<div className={styles.metadataLabel}>{meta.key}</div>
77+
<div className={styles.metadataValue}>
78+
{meta.sensitive ? (
79+
<SensitiveValue value={meta.value} />
80+
) : (
81+
meta.value
82+
)}
83+
</div>
84+
</div>
85+
)
86+
})}
6787
</div>
68-
))}
88+
89+
<Maybe condition={metadataToDisplay.length > 4}>
90+
<Tooltip
91+
title={
92+
shouldDisplayAllMetadata ? "Hide metadata" : "Show all metadata"
93+
}
94+
>
95+
<IconButton
96+
onClick={() => {
97+
setShouldDisplayAllMetadata((value) => !value)
98+
}}
99+
>
100+
{shouldDisplayAllMetadata ? (
101+
<CloseDropdown margin={false} />
102+
) : (
103+
<OpenDropdown margin={false} />
104+
)}
105+
</IconButton>
106+
</Tooltip>
107+
</Maybe>
108+
</Stack>
69109
</Stack>
70110

71111
<div>
@@ -132,14 +172,15 @@ export const ResourceCard: FC<ResourceCardProps> = ({
132172
workspaceName={workspace.name}
133173
agentName={agent.name}
134174
health={app.health}
175+
appSharingLevel={app.sharing_level}
135176
/>
136177
))}
137178
</>
138179
)}
139180
{showApps && agent.status === "connecting" && (
140181
<>
141-
<Skeleton width={80} height={60} />
142-
<Skeleton width={120} height={60} />
182+
<Skeleton width={80} height={36} variant="rect" />
183+
<Skeleton width={120} height={36} variant="rect" />
143184
</>
144185
)}
145186
</Stack>
@@ -158,35 +199,40 @@ const useStyles = makeStyles((theme) => ({
158199
border: `1px solid ${theme.palette.divider}`,
159200
},
160201

202+
resourceCardProfile: {
203+
flexShrink: 0,
204+
width: "fit-content",
205+
},
206+
161207
resourceCardHeader: {
162208
padding: theme.spacing(3, 4),
163209
borderBottom: `1px solid ${theme.palette.divider}`,
164210
},
165211

166-
resourceMetadata: {
167-
padding: theme.spacing(2, 4),
168-
borderBottom: `1px solid ${theme.palette.divider}`,
169-
gap: theme.spacing(0.5, 2),
212+
metadataHeader: {
213+
display: "grid",
214+
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
215+
gap: theme.spacing(5),
216+
rowGap: theme.spacing(3),
170217
},
171218

172-
resourceHeader: {
219+
metadata: {
173220
fontSize: 16,
174221
},
175222

176-
resourceHeaderLabel: {
223+
metadataLabel: {
177224
fontSize: 12,
178225
color: theme.palette.text.secondary,
226+
textOverflow: "ellipsis",
227+
overflow: "hidden",
228+
whiteSpace: "nowrap",
179229
},
180230

181-
resourceData: {
182-
fontSize: 12,
183-
flexShrink: 0,
184-
},
185-
186-
resourceDataLabel: {
187-
fontSize: 12,
188-
color: theme.palette.text.secondary,
189-
marginRight: theme.spacing(0.75),
231+
metadataValue: {
232+
textOverflow: "ellipsis",
233+
overflow: "hidden",
234+
whiteSpace: "nowrap",
235+
userSelect: "all",
190236
},
191237

192238
agentRow: {

site/src/components/Resources/Resources.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
206206
workspaceName={workspace.name}
207207
agentName={agent.name}
208208
health={app.health}
209+
appSharingLevel={app.sharing_level}
209210
/>
210211
))}
211212
</>

site/src/components/Resources/SensitiveValue.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const SensitiveValue: React.FC<{ value: string }> = ({ value }) => {
2323

2424
return (
2525
<div className={styles.sensitiveValue}>
26-
{displayValue}
26+
<div className={styles.value}>{displayValue}</div>
2727
<Tooltip title={buttonLabel}>
2828
<IconButton
2929
className={styles.button}
@@ -41,13 +41,21 @@ export const SensitiveValue: React.FC<{ value: string }> = ({ value }) => {
4141
}
4242

4343
const useStyles = makeStyles((theme) => ({
44+
value: {
45+
// 22px is the button width
46+
width: "calc(100% - 22px)",
47+
overflow: "hidden",
48+
whiteSpace: "nowrap",
49+
textOverflow: "ellipsis",
50+
},
51+
4452
sensitiveValue: {
4553
display: "flex",
4654
alignItems: "center",
55+
gap: theme.spacing(0.5),
4756
},
4857

4958
button: {
50-
marginLeft: theme.spacing(0.5),
5159
color: "inherit",
5260

5361
"& .MuiSvgIcon-root": {

site/src/hooks/useClipboard.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useState } from "react"
2+
3+
export const useClipboard = (
4+
text: string,
5+
): { isCopied: boolean; copy: () => Promise<void> } => {
6+
const [isCopied, setIsCopied] = useState<boolean>(false)
7+
8+
const copy = async (): Promise<void> => {
9+
try {
10+
await window.navigator.clipboard.writeText(text)
11+
setIsCopied(true)
12+
window.setTimeout(() => {
13+
setIsCopied(false)
14+
}, 1000)
15+
} catch (err) {
16+
const input = document.createElement("input")
17+
input.value = text
18+
document.body.appendChild(input)
19+
input.focus()
20+
input.select()
21+
const result = document.execCommand("copy")
22+
document.body.removeChild(input)
23+
if (result) {
24+
setIsCopied(true)
25+
window.setTimeout(() => {
26+
setIsCopied(false)
27+
}, 1000)
28+
} else {
29+
const wrappedErr = new Error(
30+
"copyToClipboard: failed to copy text to clipboard",
31+
)
32+
if (err instanceof Error) {
33+
wrappedErr.stack = err.stack
34+
}
35+
console.error(wrappedErr)
36+
}
37+
}
38+
}
39+
40+
return {
41+
isCopied,
42+
copy,
43+
}
44+
}

0 commit comments

Comments
 (0)