Skip to content

Commit b225953

Browse files
feat: Add "Outdated" tooltip and "Update version" button in the Workspaces page (#2322)
1 parent e9f87f1 commit b225953

File tree

6 files changed

+460
-102
lines changed

6 files changed

+460
-102
lines changed

site/src/components/GlobalSnackbar/GlobalSnackbar.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const GlobalSnackbar: React.FC = () => {
7272

7373
return (
7474
<EnterpriseSnackbar
75+
key={notification.msg}
7576
open={open}
7677
variant={variantFromMsgType(notification.msgType)}
7778
message={

site/src/components/HelpTooltip/HelpTooltip.tsx

+69-8
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,58 @@ import Popover from "@material-ui/core/Popover"
33
import { makeStyles } from "@material-ui/core/styles"
44
import HelpIcon from "@material-ui/icons/HelpOutline"
55
import OpenInNewIcon from "@material-ui/icons/OpenInNew"
6-
import { useState } from "react"
6+
import React, { createContext, useContext, useState } from "react"
77
import { Stack } from "../Stack/Stack"
88

9+
type Icon = typeof HelpIcon
10+
911
type Size = "small" | "medium"
1012
export interface HelpTooltipProps {
1113
// Useful to test on storybook
1214
open?: boolean
1315
size?: Size
1416
}
1517

18+
const HelpTooltipContext = createContext<{ open: boolean; onClose: () => void } | undefined>(undefined)
19+
20+
const useHelpTooltip = () => {
21+
const helpTooltipContext = useContext(HelpTooltipContext)
22+
23+
if (!helpTooltipContext) {
24+
throw new Error("This hook should be used in side of the HelpTooltipContext.")
25+
}
26+
27+
return helpTooltipContext
28+
}
29+
1630
export const HelpTooltip: React.FC<HelpTooltipProps> = ({ children, open, size = "medium" }) => {
1731
const styles = useStyles({ size })
1832
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null)
1933
open = open ?? Boolean(anchorEl)
2034
const id = open ? "help-popover" : undefined
2135

36+
const onClose = () => {
37+
setAnchorEl(null)
38+
}
39+
2240
return (
2341
<>
24-
<button aria-describedby={id} className={styles.button} onClick={(event) => setAnchorEl(event.currentTarget)}>
42+
<button
43+
aria-describedby={id}
44+
className={styles.button}
45+
onClick={(event) => {
46+
event.stopPropagation()
47+
setAnchorEl(event.currentTarget)
48+
}}
49+
>
2550
<HelpIcon className={styles.icon} />
2651
</button>
2752
<Popover
2853
classes={{ paper: styles.popoverPaper }}
2954
id={id}
3055
open={open}
3156
anchorEl={anchorEl}
32-
onClose={() => {
33-
setAnchorEl(null)
34-
}}
57+
onClose={onClose}
3558
anchorOrigin={{
3659
vertical: "bottom",
3760
horizontal: "left",
@@ -41,7 +64,7 @@ export const HelpTooltip: React.FC<HelpTooltipProps> = ({ children, open, size =
4164
horizontal: "left",
4265
}}
4366
>
44-
{children}
67+
<HelpTooltipContext.Provider value={{ open, onClose }}>{children}</HelpTooltipContext.Provider>
4568
</Popover>
4669
</>
4770
)
@@ -70,6 +93,25 @@ export const HelpTooltipLink: React.FC<{ href: string }> = ({ children, href })
7093
)
7194
}
7295

96+
export const HelpTooltipAction: React.FC<{ icon: Icon; onClick: () => void }> = ({ children, icon: Icon, onClick }) => {
97+
const styles = useStyles()
98+
const tooltip = useHelpTooltip()
99+
100+
return (
101+
<button
102+
className={styles.action}
103+
onClick={(event) => {
104+
event.stopPropagation()
105+
onClick()
106+
tooltip.onClose()
107+
}}
108+
>
109+
<Icon className={styles.actionIcon} />
110+
{children}
111+
</button>
112+
)
113+
}
114+
73115
export const HelpTooltipLinksGroup: React.FC = ({ children }) => {
74116
const styles = useStyles()
75117

@@ -110,11 +152,12 @@ const useStyles = makeStyles((theme) => ({
110152
padding: 0,
111153
border: 0,
112154
background: "transparent",
113-
color: theme.palette.text.secondary,
155+
color: theme.palette.text.primary,
156+
opacity: 0.5,
114157
cursor: "pointer",
115158

116159
"&:hover": {
117-
color: theme.palette.text.primary,
160+
opacity: 0.75,
118161
},
119162
},
120163

@@ -156,4 +199,22 @@ const useStyles = makeStyles((theme) => ({
156199
linksGroup: {
157200
marginTop: theme.spacing(2),
158201
},
202+
203+
action: {
204+
display: "flex",
205+
alignItems: "center",
206+
background: "none",
207+
border: 0,
208+
color: theme.palette.primary.light,
209+
padding: 0,
210+
cursor: "pointer",
211+
fontSize: 14,
212+
},
213+
214+
actionIcon: {
215+
color: "inherit",
216+
width: 14,
217+
height: 14,
218+
marginRight: theme.spacing(1),
219+
},
159220
}))

site/src/pages/WorkspacesPage/WorkspacesPage.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import { WorkspacesPageView } from "./WorkspacesPageView"
1010
const WorkspacesPage: FC = () => {
1111
const [workspacesState, send] = useMachine(workspacesMachine)
1212
const [searchParams, setSearchParams] = useSearchParams()
13+
const { workspaceRefs } = workspacesState.context
1314

1415
useEffect(() => {
1516
const filter = searchParams.get("filter")
1617
const query = filter !== null ? filter : workspaceFilterQuery.me
1718

1819
send({
19-
type: "SET_FILTER",
20+
type: "GET_WORKSPACES",
2021
query,
2122
})
2223
}, [searchParams, send])
@@ -30,7 +31,7 @@ const WorkspacesPage: FC = () => {
3031
<WorkspacesPageView
3132
filter={workspacesState.context.filter}
3233
loading={workspacesState.hasTag("loading")}
33-
workspaces={workspacesState.context.workspaces}
34+
workspaceRefs={workspaceRefs}
3435
onFilter={(query) => {
3536
searchParams.set("filter", query)
3637
setSearchParams(searchParams)

site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { ComponentMeta, Story } from "@storybook/react"
2+
import { spawn } from "xstate"
23
import { ProvisionerJobStatus, Workspace, WorkspaceTransition } from "../../api/typesGenerated"
34
import { MockWorkspace } from "../../testHelpers/entities"
45
import { workspaceFilterQuery } from "../../util/workspace"
6+
import { workspaceItemMachine } from "../../xServices/workspaces/workspacesXService"
57
import { WorkspacesPageView, WorkspacesPageViewProps } from "./WorkspacesPageView"
68

79
export default {
@@ -14,9 +16,11 @@ const Template: Story<WorkspacesPageViewProps> = (args) => <WorkspacesPageView {
1416
const createWorkspaceWithStatus = (
1517
status: ProvisionerJobStatus,
1618
transition: WorkspaceTransition = "start",
19+
outdated = false,
1720
): Workspace => {
1821
return {
1922
...MockWorkspace,
23+
outdated,
2024
latest_build: {
2125
...MockWorkspace.latest_build,
2226
transition,
@@ -41,22 +45,29 @@ const workspaces: { [key in ProvisionerJobStatus]: Workspace } = {
4145

4246
export const AllStates = Template.bind({})
4347
AllStates.args = {
44-
workspaces: [
48+
workspaceRefs: [
4549
...Object.values(workspaces),
4650
createWorkspaceWithStatus("running", "stop"),
4751
createWorkspaceWithStatus("succeeded", "stop"),
4852
createWorkspaceWithStatus("running", "delete"),
49-
],
53+
].map((data) => spawn(workspaceItemMachine.withContext({ data }))),
54+
}
55+
56+
export const Outdated = Template.bind({})
57+
Outdated.args = {
58+
workspaceRefs: [createWorkspaceWithStatus("running", "stop", true)].map((data) =>
59+
spawn(workspaceItemMachine.withContext({ data })),
60+
),
5061
}
5162

5263
export const OwnerHasNoWorkspaces = Template.bind({})
5364
OwnerHasNoWorkspaces.args = {
54-
workspaces: [],
65+
workspaceRefs: [],
5566
filter: workspaceFilterQuery.me,
5667
}
5768

5869
export const NoResults = Template.bind({})
5970
NoResults.args = {
60-
workspaces: [],
71+
workspaceRefs: [],
6172
filter: "searchtearmwithnoresults",
6273
}

0 commit comments

Comments
 (0)