feat: improve update button visibility (#3115)

* feat: give update button primary focus when applicable

resolves #3024

* added update tooltip

* cleanup

* prettier

* PR feedback
This commit is contained in:
Kira Pilot
2022-07-22 14:28:52 -04:00
committed by GitHub
parent 2dd98c7ec8
commit 471564df7d
16 changed files with 131 additions and 79 deletions

View File

@ -1,12 +1,9 @@
import { makeStyles } from "@material-ui/core/styles" import { makeStyles } from "@material-ui/core/styles"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { FC } from "react" import { FC } from "react"
import { createDayString } from "util/createDayString"
import { Template, TemplateVersion } from "../../api/typesGenerated" import { Template, TemplateVersion } from "../../api/typesGenerated"
import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants" import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants"
dayjs.extend(relativeTime)
const Language = { const Language = {
usedByLabel: "Used by", usedByLabel: "Used by",
activeVersionLabel: "Active version", activeVersionLabel: "Active version",
@ -45,7 +42,7 @@ export const TemplateStats: FC<TemplateStatsProps> = ({ template, activeVersion
<div className={styles.statItem}> <div className={styles.statItem}>
<span className={styles.statsLabel}>{Language.lastUpdateLabel}</span> <span className={styles.statsLabel}>{Language.lastUpdateLabel}</span>
<span className={styles.statsValue} data-chromatic="ignore"> <span className={styles.statsValue} data-chromatic="ignore">
{dayjs().to(dayjs(template.updated_at))} {createDayString(template.updated_at)}
</span> </span>
</div> </div>
<div className={styles.statsDivider} /> <div className={styles.statsDivider} />

View File

@ -110,16 +110,17 @@ export const HelpTooltipLink: React.FC<{ href: string }> = ({ children, href })
) )
} }
export const HelpTooltipAction: React.FC<{ icon: Icon; onClick: () => void }> = ({ export const HelpTooltipAction: React.FC<{
children, icon: Icon
icon: Icon, onClick: () => void
onClick, ariaLabel?: string
}) => { }> = ({ children, icon: Icon, onClick, ariaLabel }) => {
const styles = useStyles() const styles = useStyles()
const tooltip = useHelpTooltip() const tooltip = useHelpTooltip()
return ( return (
<button <button
aria-label={ariaLabel ?? ""}
className={styles.action} className={styles.action}
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()

View File

@ -8,7 +8,7 @@ import {
HelpTooltipTitle, HelpTooltipTitle,
} from "./HelpTooltip" } from "./HelpTooltip"
const Language = { export const Language = {
outdatedLabel: "Outdated", outdatedLabel: "Outdated",
versionTooltipText: "This workspace version is outdated and a newer version is available.", versionTooltipText: "This workspace version is outdated and a newer version is available.",
updateVersionLabel: "Update version", updateVersionLabel: "Update version",
@ -16,15 +16,16 @@ const Language = {
interface TooltipProps { interface TooltipProps {
onUpdateVersion: () => void onUpdateVersion: () => void
ariaLabel?: string
} }
export const OutdatedHelpTooltip: FC<TooltipProps> = ({ onUpdateVersion }) => { export const OutdatedHelpTooltip: FC<TooltipProps> = ({ onUpdateVersion, ariaLabel }) => {
return ( return (
<HelpTooltip size="small"> <HelpTooltip size="small">
<HelpTooltipTitle>{Language.outdatedLabel}</HelpTooltipTitle> <HelpTooltipTitle>{Language.outdatedLabel}</HelpTooltipTitle>
<HelpTooltipText>{Language.versionTooltipText}</HelpTooltipText> <HelpTooltipText>{Language.versionTooltipText}</HelpTooltipText>
<HelpTooltipLinksGroup> <HelpTooltipLinksGroup>
<HelpTooltipAction icon={RefreshIcon} onClick={onUpdateVersion}> <HelpTooltipAction icon={RefreshIcon} onClick={onUpdateVersion} ariaLabel={ariaLabel}>
{Language.updateVersionLabel} {Language.updateVersionLabel}
</HelpTooltipAction> </HelpTooltipAction>
</HelpTooltipLinksGroup> </HelpTooltipLinksGroup>

View File

@ -94,7 +94,7 @@ export const Workspace: FC<WorkspaceProps> = ({
handleClick={() => navigate(`/templates`)} handleClick={() => navigate(`/templates`)}
/> />
<WorkspaceStats workspace={workspace} /> <WorkspaceStats workspace={workspace} handleUpdate={handleUpdate} />
{!!resources && !!resources.length && ( {!!resources && !!resources.length && (
<Resources <Resources

View File

@ -6,8 +6,6 @@ import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
import HighlightOffIcon from "@material-ui/icons/HighlightOff" import HighlightOffIcon from "@material-ui/icons/HighlightOff"
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
import { FC } from "react" import { FC } from "react"
import { Workspace } from "../../api/typesGenerated"
import { WorkspaceStatus } from "../../util/workspace"
import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton" import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton"
export const Language = { export const Language = {
@ -22,6 +20,16 @@ interface WorkspaceAction {
handleAction: () => void handleAction: () => void
} }
export const UpdateButton: FC<WorkspaceAction> = ({ handleAction }) => {
const styles = useStyles()
return (
<Button className={styles.actionButton} startIcon={<CloudQueueIcon />} onClick={handleAction}>
{Language.update}
</Button>
)
}
export const StartButton: FC<WorkspaceAction> = ({ handleAction }) => { export const StartButton: FC<WorkspaceAction> = ({ handleAction }) => {
const styles = useStyles() const styles = useStyles()
@ -61,36 +69,6 @@ export const DeleteButton: FC<WorkspaceAction> = ({ handleAction }) => {
) )
} }
type UpdateAction = WorkspaceAction & {
workspace: Workspace
workspaceStatus: WorkspaceStatus
}
export const UpdateButton: FC<UpdateAction> = ({ handleAction, workspace, workspaceStatus }) => {
const styles = useStyles()
/**
* Jobs submitted while another job is in progress will be discarded,
* so check whether workspace job status has reached completion (whether successful or not).
*/
const canAcceptJobs = (workspaceStatus: WorkspaceStatus) =>
["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus)
return (
<>
{workspace.outdated && canAcceptJobs(workspaceStatus) && (
<Button
className={styles.actionButton}
startIcon={<CloudQueueIcon />}
onClick={handleAction}
>
{Language.update}
</Button>
)}
</>
)
}
export const CancelButton: FC<WorkspaceAction> = ({ handleAction }) => { export const CancelButton: FC<WorkspaceAction> = ({ handleAction }) => {
const styles = useStyles() const styles = useStyles()

View File

@ -79,11 +79,11 @@ describe("WorkspaceActions", () => {
}) })
}) })
describe("when the workspace is outdated", () => { describe("when the workspace is outdated", () => {
it("primary is start; secondary are delete, update", async () => { it("primary is update; secondary are start, delete", async () => {
await renderAndClick({ workspace: Mocks.MockOutdatedWorkspace }) await renderAndClick({ workspace: Mocks.MockOutdatedWorkspace })
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.start) expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.update)
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.start)
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete) expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.update)
}) })
}) })
}) })

View File

@ -1,13 +1,20 @@
import Button from "@material-ui/core/Button" import Button from "@material-ui/core/Button"
import Popover from "@material-ui/core/Popover" import Popover from "@material-ui/core/Popover"
import { makeStyles } from "@material-ui/core/styles" import { makeStyles } from "@material-ui/core/styles"
import { FC, ReactNode, useEffect, useRef, useState } from "react" import { FC, ReactNode, useEffect, useMemo, useRef, useState } from "react"
import { Workspace } from "../../api/typesGenerated" import { Workspace } from "../../api/typesGenerated"
import { getWorkspaceStatus } from "../../util/workspace" import { getWorkspaceStatus, WorkspaceStatus } from "../../util/workspace"
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows" import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
import { CancelButton, DeleteButton, StartButton, StopButton, UpdateButton } from "./ActionCtas" import { CancelButton, DeleteButton, StartButton, StopButton, UpdateButton } from "./ActionCtas"
import { ButtonTypesEnum, WorkspaceStateActions, WorkspaceStateEnum } from "./constants" import { ButtonTypesEnum, WorkspaceStateActions, WorkspaceStateEnum } from "./constants"
/**
* Jobs submitted while another job is in progress will be discarded,
* so check whether workspace job status has reached completion (whether successful or not).
*/
const canAcceptJobs = (workspaceStatus: WorkspaceStatus) =>
["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus)
export interface WorkspaceActionsProps { export interface WorkspaceActionsProps {
workspace: Workspace workspace: Workspace
handleStart: () => void handleStart: () => void
@ -34,7 +41,23 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
workspace.latest_build, workspace.latest_build,
) )
const workspaceState = WorkspaceStateEnum[workspaceStatus] const workspaceState = WorkspaceStateEnum[workspaceStatus]
const actions = WorkspaceStateActions[workspaceState]
const canBeUpdated = workspace.outdated && canAcceptJobs(workspaceStatus)
// actions are the primary and secondary CTAs that appear in the workspace actions dropdown
const actions = useMemo(() => {
if (!canBeUpdated) {
return WorkspaceStateActions[workspaceState]
}
// if an update is available, we make the update button the primary CTA
// and move the former primary CTA to the secondary actions list
const updatedActions = { ...WorkspaceStateActions[workspaceState] }
updatedActions.secondary.unshift(updatedActions.primary)
updatedActions.primary = ButtonTypesEnum.update
return updatedActions
}, [canBeUpdated, workspaceState])
/** /**
* Ensures we close the popover before calling any action handler * Ensures we close the popover before calling any action handler
@ -58,16 +81,10 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
// A mapping of button type to the corresponding React component // A mapping of button type to the corresponding React component
const buttonMapping: ButtonMapping = { const buttonMapping: ButtonMapping = {
[ButtonTypesEnum.update]: <UpdateButton handleAction={handleUpdate} />,
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />, [ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
[ButtonTypesEnum.stop]: <StopButton handleAction={handleStop} />, [ButtonTypesEnum.stop]: <StopButton handleAction={handleStop} />,
[ButtonTypesEnum.delete]: <DeleteButton handleAction={handleDelete} />, [ButtonTypesEnum.delete]: <DeleteButton handleAction={handleDelete} />,
[ButtonTypesEnum.update]: (
<UpdateButton
handleAction={handleUpdate}
workspace={workspace}
workspaceStatus={workspaceStatus}
/>
),
[ButtonTypesEnum.cancel]: <CancelButton handleAction={handleCancel} />, [ButtonTypesEnum.cancel]: <CancelButton handleAction={handleCancel} />,
[ButtonTypesEnum.canceling]: disabledButton, [ButtonTypesEnum.canceling]: disabledButton,
[ButtonTypesEnum.disabled]: disabledButton, [ButtonTypesEnum.disabled]: disabledButton,

View File

@ -45,7 +45,7 @@ export const WorkspaceStateActions: StateActionsType = {
}, },
[WorkspaceStateEnum.started]: { [WorkspaceStateEnum.started]: {
primary: ButtonTypesEnum.stop, primary: ButtonTypesEnum.stop,
secondary: [ButtonTypesEnum.delete, ButtonTypesEnum.update], secondary: [ButtonTypesEnum.delete],
}, },
[WorkspaceStateEnum.stopping]: { [WorkspaceStateEnum.stopping]: {
primary: ButtonTypesEnum.cancel, primary: ButtonTypesEnum.cancel,
@ -53,16 +53,16 @@ export const WorkspaceStateActions: StateActionsType = {
}, },
[WorkspaceStateEnum.stopped]: { [WorkspaceStateEnum.stopped]: {
primary: ButtonTypesEnum.start, primary: ButtonTypesEnum.start,
secondary: [ButtonTypesEnum.delete, ButtonTypesEnum.update], secondary: [ButtonTypesEnum.delete],
}, },
[WorkspaceStateEnum.canceled]: { [WorkspaceStateEnum.canceled]: {
primary: ButtonTypesEnum.start, primary: ButtonTypesEnum.start,
secondary: [ButtonTypesEnum.stop, ButtonTypesEnum.delete, ButtonTypesEnum.update], secondary: [ButtonTypesEnum.stop, ButtonTypesEnum.delete],
}, },
// in the case of an error // in the case of an error
[WorkspaceStateEnum.error]: { [WorkspaceStateEnum.error]: {
primary: ButtonTypesEnum.start, // give the user the ability to start a workspace again primary: ButtonTypesEnum.start, // give the user the ability to start a workspace again
secondary: [ButtonTypesEnum.delete, ButtonTypesEnum.update], // allows the user to delete or update secondary: [ButtonTypesEnum.delete], // allows the user to delete
}, },
/** /**
* disabled states * disabled states

View File

@ -0,0 +1,31 @@
import { fireEvent, screen } from "@testing-library/react"
import { Language } from "components/Tooltips/OutdatedHelpTooltip"
import { WorkspaceStats } from "components/WorkspaceStats/WorkspaceStats"
import { MockOutdatedWorkspace } from "testHelpers/entities"
import { renderWithAuth } from "testHelpers/renderHelpers"
import * as CreateDayString from "util/createDayString"
describe("WorkspaceStats", () => {
it("shows an outdated tooltip", async () => {
// Mocking the dayjs module within the createDayString file
const mock = jest.spyOn(CreateDayString, "createDayString")
mock.mockImplementation(() => "a minute ago")
const handleUpdateMock = jest.fn()
renderWithAuth(
<WorkspaceStats handleUpdate={handleUpdateMock} workspace={MockOutdatedWorkspace} />,
{
route: `/@${MockOutdatedWorkspace.owner_name}/${MockOutdatedWorkspace.name}`,
path: "/@:username/:workspace",
},
)
const tooltipButton = await screen.findByRole("button")
fireEvent.click(tooltipButton)
expect(await screen.findByText(Language.versionTooltipText)).toBeInTheDocument()
const updateButton = screen.getByRole("button", {
name: "update version",
})
fireEvent.click(updateButton)
expect(handleUpdateMock).toBeCalledTimes(1)
})
})

View File

@ -1,12 +1,13 @@
import Link from "@material-ui/core/Link" import Link from "@material-ui/core/Link"
import { makeStyles, useTheme } from "@material-ui/core/styles" import { makeStyles, useTheme } from "@material-ui/core/styles"
import dayjs from "dayjs" import { OutdatedHelpTooltip } from "components/Tooltips"
import { FC } from "react" import { FC } from "react"
import { Link as RouterLink } from "react-router-dom" import { Link as RouterLink } from "react-router-dom"
import { combineClasses } from "util/combineClasses"
import { createDayString } from "util/createDayString"
import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "util/workspace"
import { Workspace } from "../../api/typesGenerated" import { Workspace } from "../../api/typesGenerated"
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
import { combineClasses } from "../../util/combineClasses"
import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "../../util/workspace"
const Language = { const Language = {
workspaceDetails: "Workspace Details", workspaceDetails: "Workspace Details",
@ -21,9 +22,10 @@ const Language = {
export interface WorkspaceStatsProps { export interface WorkspaceStatsProps {
workspace: Workspace workspace: Workspace
handleUpdate: () => void
} }
export const WorkspaceStats: FC<WorkspaceStatsProps> = ({ workspace }) => { export const WorkspaceStats: FC<WorkspaceStatsProps> = ({ workspace, handleUpdate }) => {
const styles = useStyles() const styles = useStyles()
const theme = useTheme() const theme = useTheme()
const status = getDisplayStatus(theme, workspace.latest_build) const status = getDisplayStatus(theme, workspace.latest_build)
@ -46,7 +48,10 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({ workspace }) => {
<span className={styles.statsLabel}>{Language.versionLabel}</span> <span className={styles.statsLabel}>{Language.versionLabel}</span>
<span className={styles.statsValue}> <span className={styles.statsValue}>
{workspace.outdated ? ( {workspace.outdated ? (
<span style={{ color: theme.palette.error.main }}>{Language.outdated}</span> <span className={styles.outdatedLabel}>
{Language.outdated}
<OutdatedHelpTooltip onUpdateVersion={handleUpdate} ariaLabel="update version" />
</span>
) : ( ) : (
<span style={{ color: theme.palette.text.secondary }}>{Language.upToDate}</span> <span style={{ color: theme.palette.text.secondary }}>{Language.upToDate}</span>
)} )}
@ -56,7 +61,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({ workspace }) => {
<div className={styles.statItem}> <div className={styles.statItem}>
<span className={styles.statsLabel}>{Language.lastBuiltLabel}</span> <span className={styles.statsLabel}>{Language.lastBuiltLabel}</span>
<span className={styles.statsValue} data-chromatic="ignore"> <span className={styles.statsValue} data-chromatic="ignore">
{dayjs().to(dayjs(workspace.latest_build.created_at))} {createDayString(workspace.latest_build.created_at)}
</span> </span>
</div> </div>
<div className={styles.statsDivider} /> <div className={styles.statsDivider} />
@ -133,4 +138,10 @@ const useStyles = makeStyles((theme) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontWeight: 600, fontWeight: 600,
}, },
outdatedLabel: {
color: theme.palette.error.main,
display: "flex",
alignItems: "center",
gap: theme.spacing(0.5),
},
})) }))

View File

@ -3,10 +3,9 @@ import TableRow from "@material-ui/core/TableRow"
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
import useTheme from "@material-ui/styles/useTheme" import useTheme from "@material-ui/styles/useTheme"
import { useActor } from "@xstate/react" import { useActor } from "@xstate/react"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { FC } from "react" import { FC } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { createDayString } from "util/createDayString"
import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "../../util/workspace" import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "../../util/workspace"
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService" import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
import { AvatarData } from "../AvatarData/AvatarData" import { AvatarData } from "../AvatarData/AvatarData"
@ -18,8 +17,6 @@ import {
import { TableCellLink } from "../TableCellLink/TableCellLink" import { TableCellLink } from "../TableCellLink/TableCellLink"
import { OutdatedHelpTooltip } from "../Tooltips" import { OutdatedHelpTooltip } from "../Tooltips"
dayjs.extend(relativeTime)
const Language = { const Language = {
upToDateLabel: "Up to date", upToDateLabel: "Up to date",
outdatedLabel: "Outdated", outdatedLabel: "Outdated",
@ -58,7 +55,7 @@ export const WorkspacesRow: FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({ w
<TableCellLink to={workspacePageLink}> <TableCellLink to={workspacePageLink}>
<AvatarData <AvatarData
title={initiatedBy} title={initiatedBy}
subtitle={dayjs().to(dayjs(workspace.latest_build.created_at))} subtitle={createDayString(workspace.latest_build.created_at)}
/> />
</TableCellLink> </TableCellLink>
<TableCellLink to={workspacePageLink}> <TableCellLink to={workspacePageLink}>

View File

@ -1,4 +1,5 @@
import { screen } from "@testing-library/react" import { screen } from "@testing-library/react"
import * as CreateDayString from "util/createDayString"
import { import {
MockTemplate, MockTemplate,
MockTemplateVersion, MockTemplateVersion,
@ -9,6 +10,10 @@ import { TemplatePage } from "./TemplatePage"
describe("TemplatePage", () => { describe("TemplatePage", () => {
it("shows the template name, readme and resources", async () => { it("shows the template name, readme and resources", async () => {
// Mocking the dayjs module within the createDayString file
const mock = jest.spyOn(CreateDayString, "createDayString")
mock.mockImplementation(() => "a minute ago")
renderWithAuth(<TemplatePage />, { renderWithAuth(<TemplatePage />, {
route: `/templates/${MockTemplate.id}`, route: `/templates/${MockTemplate.id}`,
path: "/templates/:template", path: "/templates/:template",

View File

@ -1,5 +1,6 @@
import { screen } from "@testing-library/react" import { screen } from "@testing-library/react"
import { rest } from "msw" import { rest } from "msw"
import * as CreateDayString from "util/createDayString"
import { MockTemplate } from "../../testHelpers/entities" import { MockTemplate } from "../../testHelpers/entities"
import { history, render } from "../../testHelpers/renderHelpers" import { history, render } from "../../testHelpers/renderHelpers"
import { server } from "../../testHelpers/server" import { server } from "../../testHelpers/server"
@ -8,6 +9,9 @@ import { Language } from "./TemplatesPageView"
describe("TemplatesPage", () => { describe("TemplatesPage", () => {
beforeEach(() => { beforeEach(() => {
// Mocking the dayjs module within the createDayString file
const mock = jest.spyOn(CreateDayString, "createDayString")
mock.mockImplementation(() => "a minute ago")
history.replace("/workspaces") history.replace("/workspaces")
}) })

View File

@ -6,10 +6,9 @@ import TableCell from "@material-ui/core/TableCell"
import TableHead from "@material-ui/core/TableHead" import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow" import TableRow from "@material-ui/core/TableRow"
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { FC } from "react" import { FC } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { createDayString } from "util/createDayString"
import * as TypesGen from "../../api/typesGenerated" import * as TypesGen from "../../api/typesGenerated"
import { AvatarData } from "../../components/AvatarData/AvatarData" import { AvatarData } from "../../components/AvatarData/AvatarData"
import { CodeExample } from "../../components/CodeExample/CodeExample" import { CodeExample } from "../../components/CodeExample/CodeExample"
@ -31,8 +30,6 @@ import {
HelpTooltipTitle, HelpTooltipTitle,
} from "../../components/Tooltips/HelpTooltip/HelpTooltip" } from "../../components/Tooltips/HelpTooltip/HelpTooltip"
dayjs.extend(relativeTime)
export const Language = { export const Language = {
developerCount: (ownerCount: number): string => { developerCount: (ownerCount: number): string => {
return `${ownerCount} developer${ownerCount !== 1 ? "s" : ""}` return `${ownerCount} developer${ownerCount !== 1 ? "s" : ""}`
@ -151,7 +148,7 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
</TableCellLink> </TableCellLink>
<TableCellLink data-chromatic="ignore" to={templatePageLink}> <TableCellLink data-chromatic="ignore" to={templatePageLink}>
{dayjs().to(dayjs(template.updated_at))} {createDayString(template.updated_at)}
</TableCellLink> </TableCellLink>
<TableCellLink to={templatePageLink}>{template.created_by_name}</TableCellLink> <TableCellLink to={templatePageLink}>{template.created_by_name}</TableCellLink>
<TableCellLink to={templatePageLink}> <TableCellLink to={templatePageLink}>

View File

@ -1,5 +1,6 @@
import { screen } from "@testing-library/react" import { screen } from "@testing-library/react"
import { rest } from "msw" import { rest } from "msw"
import * as CreateDayString from "util/createDayString"
import { Language as WorkspacesTableBodyLanguage } from "../../components/WorkspacesTable/WorkspacesTableBody" import { Language as WorkspacesTableBodyLanguage } from "../../components/WorkspacesTable/WorkspacesTableBody"
import { MockWorkspace } from "../../testHelpers/entities" import { MockWorkspace } from "../../testHelpers/entities"
import { history, render } from "../../testHelpers/renderHelpers" import { history, render } from "../../testHelpers/renderHelpers"
@ -9,6 +10,9 @@ import WorkspacesPage from "./WorkspacesPage"
describe("WorkspacesPage", () => { describe("WorkspacesPage", () => {
beforeEach(() => { beforeEach(() => {
history.replace("/workspaces") history.replace("/workspaces")
// Mocking the dayjs module within the createDayString file
const mock = jest.spyOn(CreateDayString, "createDayString")
mock.mockImplementation(() => "a minute ago")
}) })
it("renders an empty workspaces page", async () => { it("renders an empty workspaces page", async () => {

View File

@ -0,0 +1,9 @@
import dayjs from "dayjs"
/**
* Returns a human-readable string describing the passing of time
* Broken into its own module for testing purposes
*/
export function createDayString(time: string): string {
return dayjs().to(dayjs(time))
}