Skip to content

Commit 471564d

Browse files
authored
feat: improve update button visibility (#3115)
* feat: give update button primary focus when applicable resolves #3024 * added update tooltip * cleanup * prettier * PR feedback
1 parent 2dd98c7 commit 471564d

File tree

16 files changed

+131
-79
lines changed

16 files changed

+131
-79
lines changed

site/src/components/TemplateStats/TemplateStats.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { makeStyles } from "@material-ui/core/styles"
2-
import dayjs from "dayjs"
3-
import relativeTime from "dayjs/plugin/relativeTime"
42
import { FC } from "react"
3+
import { createDayString } from "util/createDayString"
54
import { Template, TemplateVersion } from "../../api/typesGenerated"
65
import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants"
76

8-
dayjs.extend(relativeTime)
9-
107
const Language = {
118
usedByLabel: "Used by",
129
activeVersionLabel: "Active version",
@@ -45,7 +42,7 @@ export const TemplateStats: FC<TemplateStatsProps> = ({ template, activeVersion
4542
<div className={styles.statItem}>
4643
<span className={styles.statsLabel}>{Language.lastUpdateLabel}</span>
4744
<span className={styles.statsValue} data-chromatic="ignore">
48-
{dayjs().to(dayjs(template.updated_at))}
45+
{createDayString(template.updated_at)}
4946
</span>
5047
</div>
5148
<div className={styles.statsDivider} />

site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,17 @@ export const HelpTooltipLink: React.FC<{ href: string }> = ({ children, href })
110110
)
111111
}
112112

113-
export const HelpTooltipAction: React.FC<{ icon: Icon; onClick: () => void }> = ({
114-
children,
115-
icon: Icon,
116-
onClick,
117-
}) => {
113+
export const HelpTooltipAction: React.FC<{
114+
icon: Icon
115+
onClick: () => void
116+
ariaLabel?: string
117+
}> = ({ children, icon: Icon, onClick, ariaLabel }) => {
118118
const styles = useStyles()
119119
const tooltip = useHelpTooltip()
120120

121121
return (
122122
<button
123+
aria-label={ariaLabel ?? ""}
123124
className={styles.action}
124125
onClick={(event) => {
125126
event.stopPropagation()

site/src/components/Tooltips/OutdatedHelpTooltip.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,24 @@ import {
88
HelpTooltipTitle,
99
} from "./HelpTooltip"
1010

11-
const Language = {
11+
export const Language = {
1212
outdatedLabel: "Outdated",
1313
versionTooltipText: "This workspace version is outdated and a newer version is available.",
1414
updateVersionLabel: "Update version",
1515
}
1616

1717
interface TooltipProps {
1818
onUpdateVersion: () => void
19+
ariaLabel?: string
1920
}
2021

21-
export const OutdatedHelpTooltip: FC<TooltipProps> = ({ onUpdateVersion }) => {
22+
export const OutdatedHelpTooltip: FC<TooltipProps> = ({ onUpdateVersion, ariaLabel }) => {
2223
return (
2324
<HelpTooltip size="small">
2425
<HelpTooltipTitle>{Language.outdatedLabel}</HelpTooltipTitle>
2526
<HelpTooltipText>{Language.versionTooltipText}</HelpTooltipText>
2627
<HelpTooltipLinksGroup>
27-
<HelpTooltipAction icon={RefreshIcon} onClick={onUpdateVersion}>
28+
<HelpTooltipAction icon={RefreshIcon} onClick={onUpdateVersion} ariaLabel={ariaLabel}>
2829
{Language.updateVersionLabel}
2930
</HelpTooltipAction>
3031
</HelpTooltipLinksGroup>

site/src/components/Workspace/Workspace.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export const Workspace: FC<WorkspaceProps> = ({
9494
handleClick={() => navigate(`/templates`)}
9595
/>
9696

97-
<WorkspaceStats workspace={workspace} />
97+
<WorkspaceStats workspace={workspace} handleUpdate={handleUpdate} />
9898

9999
{!!resources && !!resources.length && (
100100
<Resources

site/src/components/WorkspaceActions/ActionCtas.tsx

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
66
import HighlightOffIcon from "@material-ui/icons/HighlightOff"
77
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
88
import { FC } from "react"
9-
import { Workspace } from "../../api/typesGenerated"
10-
import { WorkspaceStatus } from "../../util/workspace"
119
import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton"
1210

1311
export const Language = {
@@ -22,6 +20,16 @@ interface WorkspaceAction {
2220
handleAction: () => void
2321
}
2422

23+
export const UpdateButton: FC<WorkspaceAction> = ({ handleAction }) => {
24+
const styles = useStyles()
25+
26+
return (
27+
<Button className={styles.actionButton} startIcon={<CloudQueueIcon />} onClick={handleAction}>
28+
{Language.update}
29+
</Button>
30+
)
31+
}
32+
2533
export const StartButton: FC<WorkspaceAction> = ({ handleAction }) => {
2634
const styles = useStyles()
2735

@@ -61,36 +69,6 @@ export const DeleteButton: FC<WorkspaceAction> = ({ handleAction }) => {
6169
)
6270
}
6371

64-
type UpdateAction = WorkspaceAction & {
65-
workspace: Workspace
66-
workspaceStatus: WorkspaceStatus
67-
}
68-
69-
export const UpdateButton: FC<UpdateAction> = ({ handleAction, workspace, workspaceStatus }) => {
70-
const styles = useStyles()
71-
72-
/**
73-
* Jobs submitted while another job is in progress will be discarded,
74-
* so check whether workspace job status has reached completion (whether successful or not).
75-
*/
76-
const canAcceptJobs = (workspaceStatus: WorkspaceStatus) =>
77-
["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus)
78-
79-
return (
80-
<>
81-
{workspace.outdated && canAcceptJobs(workspaceStatus) && (
82-
<Button
83-
className={styles.actionButton}
84-
startIcon={<CloudQueueIcon />}
85-
onClick={handleAction}
86-
>
87-
{Language.update}
88-
</Button>
89-
)}
90-
</>
91-
)
92-
}
93-
9472
export const CancelButton: FC<WorkspaceAction> = ({ handleAction }) => {
9573
const styles = useStyles()
9674

site/src/components/WorkspaceActions/WorkspaceActions.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ describe("WorkspaceActions", () => {
7979
})
8080
})
8181
describe("when the workspace is outdated", () => {
82-
it("primary is start; secondary are delete, update", async () => {
82+
it("primary is update; secondary are start, delete", async () => {
8383
await renderAndClick({ workspace: Mocks.MockOutdatedWorkspace })
84-
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.start)
84+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.update)
85+
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.start)
8586
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
86-
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.update)
8787
})
8888
})
8989
})

site/src/components/WorkspaceActions/WorkspaceActions.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import Button from "@material-ui/core/Button"
22
import Popover from "@material-ui/core/Popover"
33
import { makeStyles } from "@material-ui/core/styles"
4-
import { FC, ReactNode, useEffect, useRef, useState } from "react"
4+
import { FC, ReactNode, useEffect, useMemo, useRef, useState } from "react"
55
import { Workspace } from "../../api/typesGenerated"
6-
import { getWorkspaceStatus } from "../../util/workspace"
6+
import { getWorkspaceStatus, WorkspaceStatus } from "../../util/workspace"
77
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
88
import { CancelButton, DeleteButton, StartButton, StopButton, UpdateButton } from "./ActionCtas"
99
import { ButtonTypesEnum, WorkspaceStateActions, WorkspaceStateEnum } from "./constants"
1010

11+
/**
12+
* Jobs submitted while another job is in progress will be discarded,
13+
* so check whether workspace job status has reached completion (whether successful or not).
14+
*/
15+
const canAcceptJobs = (workspaceStatus: WorkspaceStatus) =>
16+
["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus)
17+
1118
export interface WorkspaceActionsProps {
1219
workspace: Workspace
1320
handleStart: () => void
@@ -34,7 +41,23 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
3441
workspace.latest_build,
3542
)
3643
const workspaceState = WorkspaceStateEnum[workspaceStatus]
37-
const actions = WorkspaceStateActions[workspaceState]
44+
45+
const canBeUpdated = workspace.outdated && canAcceptJobs(workspaceStatus)
46+
47+
// actions are the primary and secondary CTAs that appear in the workspace actions dropdown
48+
const actions = useMemo(() => {
49+
if (!canBeUpdated) {
50+
return WorkspaceStateActions[workspaceState]
51+
}
52+
53+
// if an update is available, we make the update button the primary CTA
54+
// and move the former primary CTA to the secondary actions list
55+
const updatedActions = { ...WorkspaceStateActions[workspaceState] }
56+
updatedActions.secondary.unshift(updatedActions.primary)
57+
updatedActions.primary = ButtonTypesEnum.update
58+
59+
return updatedActions
60+
}, [canBeUpdated, workspaceState])
3861

3962
/**
4063
* Ensures we close the popover before calling any action handler
@@ -58,16 +81,10 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
5881

5982
// A mapping of button type to the corresponding React component
6083
const buttonMapping: ButtonMapping = {
84+
[ButtonTypesEnum.update]: <UpdateButton handleAction={handleUpdate} />,
6185
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
6286
[ButtonTypesEnum.stop]: <StopButton handleAction={handleStop} />,
6387
[ButtonTypesEnum.delete]: <DeleteButton handleAction={handleDelete} />,
64-
[ButtonTypesEnum.update]: (
65-
<UpdateButton
66-
handleAction={handleUpdate}
67-
workspace={workspace}
68-
workspaceStatus={workspaceStatus}
69-
/>
70-
),
7188
[ButtonTypesEnum.cancel]: <CancelButton handleAction={handleCancel} />,
7289
[ButtonTypesEnum.canceling]: disabledButton,
7390
[ButtonTypesEnum.disabled]: disabledButton,

site/src/components/WorkspaceActions/constants.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,24 @@ export const WorkspaceStateActions: StateActionsType = {
4545
},
4646
[WorkspaceStateEnum.started]: {
4747
primary: ButtonTypesEnum.stop,
48-
secondary: [ButtonTypesEnum.delete, ButtonTypesEnum.update],
48+
secondary: [ButtonTypesEnum.delete],
4949
},
5050
[WorkspaceStateEnum.stopping]: {
5151
primary: ButtonTypesEnum.cancel,
5252
secondary: [],
5353
},
5454
[WorkspaceStateEnum.stopped]: {
5555
primary: ButtonTypesEnum.start,
56-
secondary: [ButtonTypesEnum.delete, ButtonTypesEnum.update],
56+
secondary: [ButtonTypesEnum.delete],
5757
},
5858
[WorkspaceStateEnum.canceled]: {
5959
primary: ButtonTypesEnum.start,
60-
secondary: [ButtonTypesEnum.stop, ButtonTypesEnum.delete, ButtonTypesEnum.update],
60+
secondary: [ButtonTypesEnum.stop, ButtonTypesEnum.delete],
6161
},
6262
// in the case of an error
6363
[WorkspaceStateEnum.error]: {
6464
primary: ButtonTypesEnum.start, // give the user the ability to start a workspace again
65-
secondary: [ButtonTypesEnum.delete, ButtonTypesEnum.update], // allows the user to delete or update
65+
secondary: [ButtonTypesEnum.delete], // allows the user to delete
6666
},
6767
/**
6868
* disabled states
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { fireEvent, screen } from "@testing-library/react"
2+
import { Language } from "components/Tooltips/OutdatedHelpTooltip"
3+
import { WorkspaceStats } from "components/WorkspaceStats/WorkspaceStats"
4+
import { MockOutdatedWorkspace } from "testHelpers/entities"
5+
import { renderWithAuth } from "testHelpers/renderHelpers"
6+
import * as CreateDayString from "util/createDayString"
7+
8+
describe("WorkspaceStats", () => {
9+
it("shows an outdated tooltip", async () => {
10+
// Mocking the dayjs module within the createDayString file
11+
const mock = jest.spyOn(CreateDayString, "createDayString")
12+
mock.mockImplementation(() => "a minute ago")
13+
14+
const handleUpdateMock = jest.fn()
15+
renderWithAuth(
16+
<WorkspaceStats handleUpdate={handleUpdateMock} workspace={MockOutdatedWorkspace} />,
17+
{
18+
route: `/@${MockOutdatedWorkspace.owner_name}/${MockOutdatedWorkspace.name}`,
19+
path: "/@:username/:workspace",
20+
},
21+
)
22+
const tooltipButton = await screen.findByRole("button")
23+
fireEvent.click(tooltipButton)
24+
expect(await screen.findByText(Language.versionTooltipText)).toBeInTheDocument()
25+
const updateButton = screen.getByRole("button", {
26+
name: "update version",
27+
})
28+
fireEvent.click(updateButton)
29+
expect(handleUpdateMock).toBeCalledTimes(1)
30+
})
31+
})

site/src/components/WorkspaceStats/WorkspaceStats.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import Link from "@material-ui/core/Link"
22
import { makeStyles, useTheme } from "@material-ui/core/styles"
3-
import dayjs from "dayjs"
3+
import { OutdatedHelpTooltip } from "components/Tooltips"
44
import { FC } from "react"
55
import { Link as RouterLink } from "react-router-dom"
6+
import { combineClasses } from "util/combineClasses"
7+
import { createDayString } from "util/createDayString"
8+
import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "util/workspace"
69
import { Workspace } from "../../api/typesGenerated"
710
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
8-
import { combineClasses } from "../../util/combineClasses"
9-
import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "../../util/workspace"
1011

1112
const Language = {
1213
workspaceDetails: "Workspace Details",
@@ -21,9 +22,10 @@ const Language = {
2122

2223
export interface WorkspaceStatsProps {
2324
workspace: Workspace
25+
handleUpdate: () => void
2426
}
2527

26-
export const WorkspaceStats: FC<WorkspaceStatsProps> = ({ workspace }) => {
28+
export const WorkspaceStats: FC<WorkspaceStatsProps> = ({ workspace, handleUpdate }) => {
2729
const styles = useStyles()
2830
const theme = useTheme()
2931
const status = getDisplayStatus(theme, workspace.latest_build)
@@ -46,7 +48,10 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({ workspace }) => {
4648
<span className={styles.statsLabel}>{Language.versionLabel}</span>
4749
<span className={styles.statsValue}>
4850
{workspace.outdated ? (
49-
<span style={{ color: theme.palette.error.main }}>{Language.outdated}</span>
51+
<span className={styles.outdatedLabel}>
52+
{Language.outdated}
53+
<OutdatedHelpTooltip onUpdateVersion={handleUpdate} ariaLabel="update version" />
54+
</span>
5055
) : (
5156
<span style={{ color: theme.palette.text.secondary }}>{Language.upToDate}</span>
5257
)}
@@ -56,7 +61,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({ workspace }) => {
5661
<div className={styles.statItem}>
5762
<span className={styles.statsLabel}>{Language.lastBuiltLabel}</span>
5863
<span className={styles.statsValue} data-chromatic="ignore">
59-
{dayjs().to(dayjs(workspace.latest_build.created_at))}
64+
{createDayString(workspace.latest_build.created_at)}
6065
</span>
6166
</div>
6267
<div className={styles.statsDivider} />
@@ -133,4 +138,10 @@ const useStyles = makeStyles((theme) => ({
133138
color: theme.palette.text.primary,
134139
fontWeight: 600,
135140
},
141+
outdatedLabel: {
142+
color: theme.palette.error.main,
143+
display: "flex",
144+
alignItems: "center",
145+
gap: theme.spacing(0.5),
146+
},
136147
}))

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy