diff --git a/site/src/components/TemplateStats/TemplateStats.tsx b/site/src/components/TemplateStats/TemplateStats.tsx index a0b7a0722cc45..b02730b329d99 100644 --- a/site/src/components/TemplateStats/TemplateStats.tsx +++ b/site/src/components/TemplateStats/TemplateStats.tsx @@ -1,12 +1,9 @@ import { makeStyles } from "@material-ui/core/styles" -import dayjs from "dayjs" -import relativeTime from "dayjs/plugin/relativeTime" import { FC } from "react" +import { createDayString } from "util/createDayString" import { Template, TemplateVersion } from "../../api/typesGenerated" import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants" -dayjs.extend(relativeTime) - const Language = { usedByLabel: "Used by", activeVersionLabel: "Active version", @@ -45,7 +42,7 @@ export const TemplateStats: FC = ({ template, activeVersion
{Language.lastUpdateLabel} - {dayjs().to(dayjs(template.updated_at))} + {createDayString(template.updated_at)}
diff --git a/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx b/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx index d8b4e460d26af..7c70d9146743a 100644 --- a/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx @@ -110,16 +110,17 @@ export const HelpTooltipLink: React.FC<{ href: string }> = ({ children, href }) ) } -export const HelpTooltipAction: React.FC<{ icon: Icon; onClick: () => void }> = ({ - children, - icon: Icon, - onClick, -}) => { +export const HelpTooltipAction: React.FC<{ + icon: Icon + onClick: () => void + ariaLabel?: string +}> = ({ children, icon: Icon, onClick, ariaLabel }) => { const styles = useStyles() const tooltip = useHelpTooltip() return ( + ) +} + export const StartButton: FC = ({ handleAction }) => { const styles = useStyles() @@ -61,36 +69,6 @@ export const DeleteButton: FC = ({ handleAction }) => { ) } -type UpdateAction = WorkspaceAction & { - workspace: Workspace - workspaceStatus: WorkspaceStatus -} - -export const UpdateButton: FC = ({ 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) && ( - - )} - - ) -} - export const CancelButton: FC = ({ handleAction }) => { const styles = useStyles() diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx index d916393f70026..ca8b84d0e2d68 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx @@ -79,11 +79,11 @@ describe("WorkspaceActions", () => { }) }) 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 }) - 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.update) }) }) }) diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 10cc02a486270..cfb4c5be47d6f 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -1,13 +1,20 @@ import Button from "@material-ui/core/Button" import Popover from "@material-ui/core/Popover" 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 { getWorkspaceStatus } from "../../util/workspace" +import { getWorkspaceStatus, WorkspaceStatus } from "../../util/workspace" import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows" import { CancelButton, DeleteButton, StartButton, StopButton, UpdateButton } from "./ActionCtas" 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 { workspace: Workspace handleStart: () => void @@ -34,7 +41,23 @@ export const WorkspaceActions: FC = ({ workspace.latest_build, ) 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 @@ -58,16 +81,10 @@ export const WorkspaceActions: FC = ({ // A mapping of button type to the corresponding React component const buttonMapping: ButtonMapping = { + [ButtonTypesEnum.update]: , [ButtonTypesEnum.start]: , [ButtonTypesEnum.stop]: , [ButtonTypesEnum.delete]: , - [ButtonTypesEnum.update]: ( - - ), [ButtonTypesEnum.cancel]: , [ButtonTypesEnum.canceling]: disabledButton, [ButtonTypesEnum.disabled]: disabledButton, diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index 88531fefedab6..38391b1ec6218 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -45,7 +45,7 @@ export const WorkspaceStateActions: StateActionsType = { }, [WorkspaceStateEnum.started]: { primary: ButtonTypesEnum.stop, - secondary: [ButtonTypesEnum.delete, ButtonTypesEnum.update], + secondary: [ButtonTypesEnum.delete], }, [WorkspaceStateEnum.stopping]: { primary: ButtonTypesEnum.cancel, @@ -53,16 +53,16 @@ export const WorkspaceStateActions: StateActionsType = { }, [WorkspaceStateEnum.stopped]: { primary: ButtonTypesEnum.start, - secondary: [ButtonTypesEnum.delete, ButtonTypesEnum.update], + secondary: [ButtonTypesEnum.delete], }, [WorkspaceStateEnum.canceled]: { primary: ButtonTypesEnum.start, - secondary: [ButtonTypesEnum.stop, ButtonTypesEnum.delete, ButtonTypesEnum.update], + secondary: [ButtonTypesEnum.stop, ButtonTypesEnum.delete], }, // in the case of an error [WorkspaceStateEnum.error]: { 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 diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.test.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.test.tsx new file mode 100644 index 0000000000000..d7d8fc9fef3b1 --- /dev/null +++ b/site/src/components/WorkspaceStats/WorkspaceStats.test.tsx @@ -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( + , + { + 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) + }) +}) diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.tsx index bde20f8226637..094fe50c3c1fb 100644 --- a/site/src/components/WorkspaceStats/WorkspaceStats.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceStats.tsx @@ -1,12 +1,13 @@ import Link from "@material-ui/core/Link" import { makeStyles, useTheme } from "@material-ui/core/styles" -import dayjs from "dayjs" +import { OutdatedHelpTooltip } from "components/Tooltips" import { FC } from "react" 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 { MONOSPACE_FONT_FAMILY } from "../../theme/constants" -import { combineClasses } from "../../util/combineClasses" -import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "../../util/workspace" const Language = { workspaceDetails: "Workspace Details", @@ -21,9 +22,10 @@ const Language = { export interface WorkspaceStatsProps { workspace: Workspace + handleUpdate: () => void } -export const WorkspaceStats: FC = ({ workspace }) => { +export const WorkspaceStats: FC = ({ workspace, handleUpdate }) => { const styles = useStyles() const theme = useTheme() const status = getDisplayStatus(theme, workspace.latest_build) @@ -46,7 +48,10 @@ export const WorkspaceStats: FC = ({ workspace }) => { {Language.versionLabel} {workspace.outdated ? ( - {Language.outdated} + + {Language.outdated} + + ) : ( {Language.upToDate} )} @@ -56,7 +61,7 @@ export const WorkspaceStats: FC = ({ workspace }) => {
{Language.lastBuiltLabel} - {dayjs().to(dayjs(workspace.latest_build.created_at))} + {createDayString(workspace.latest_build.created_at)}
@@ -133,4 +138,10 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.primary, fontWeight: 600, }, + outdatedLabel: { + color: theme.palette.error.main, + display: "flex", + alignItems: "center", + gap: theme.spacing(0.5), + }, })) diff --git a/site/src/components/WorkspacesTable/WorkspacesRow.tsx b/site/src/components/WorkspacesTable/WorkspacesRow.tsx index bfe9b2e99e42e..24a2c534b7ebe 100644 --- a/site/src/components/WorkspacesTable/WorkspacesRow.tsx +++ b/site/src/components/WorkspacesTable/WorkspacesRow.tsx @@ -3,10 +3,9 @@ import TableRow from "@material-ui/core/TableRow" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" import useTheme from "@material-ui/styles/useTheme" import { useActor } from "@xstate/react" -import dayjs from "dayjs" -import relativeTime from "dayjs/plugin/relativeTime" import { FC } from "react" import { useNavigate } from "react-router-dom" +import { createDayString } from "util/createDayString" import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "../../util/workspace" import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService" import { AvatarData } from "../AvatarData/AvatarData" @@ -18,8 +17,6 @@ import { import { TableCellLink } from "../TableCellLink/TableCellLink" import { OutdatedHelpTooltip } from "../Tooltips" -dayjs.extend(relativeTime) - const Language = { upToDateLabel: "Up to date", outdatedLabel: "Outdated", @@ -58,7 +55,7 @@ export const WorkspacesRow: FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({ w diff --git a/site/src/pages/TemplatePage/TemplatePage.test.tsx b/site/src/pages/TemplatePage/TemplatePage.test.tsx index d1d86d5b9b74a..9dac040cbab08 100644 --- a/site/src/pages/TemplatePage/TemplatePage.test.tsx +++ b/site/src/pages/TemplatePage/TemplatePage.test.tsx @@ -1,4 +1,5 @@ import { screen } from "@testing-library/react" +import * as CreateDayString from "util/createDayString" import { MockTemplate, MockTemplateVersion, @@ -9,6 +10,10 @@ import { TemplatePage } from "./TemplatePage" describe("TemplatePage", () => { 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(, { route: `/templates/${MockTemplate.id}`, path: "/templates/:template", diff --git a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx index c2400ba1a70b3..608f9d3a95790 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx @@ -1,5 +1,6 @@ import { screen } from "@testing-library/react" import { rest } from "msw" +import * as CreateDayString from "util/createDayString" import { MockTemplate } from "../../testHelpers/entities" import { history, render } from "../../testHelpers/renderHelpers" import { server } from "../../testHelpers/server" @@ -8,6 +9,9 @@ import { Language } from "./TemplatesPageView" describe("TemplatesPage", () => { beforeEach(() => { + // Mocking the dayjs module within the createDayString file + const mock = jest.spyOn(CreateDayString, "createDayString") + mock.mockImplementation(() => "a minute ago") history.replace("/workspaces") }) diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 79d1ec294ccc1..ce822af401b24 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -6,10 +6,9 @@ import TableCell from "@material-ui/core/TableCell" import TableHead from "@material-ui/core/TableHead" import TableRow from "@material-ui/core/TableRow" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" -import dayjs from "dayjs" -import relativeTime from "dayjs/plugin/relativeTime" import { FC } from "react" import { useNavigate } from "react-router-dom" +import { createDayString } from "util/createDayString" import * as TypesGen from "../../api/typesGenerated" import { AvatarData } from "../../components/AvatarData/AvatarData" import { CodeExample } from "../../components/CodeExample/CodeExample" @@ -31,8 +30,6 @@ import { HelpTooltipTitle, } from "../../components/Tooltips/HelpTooltip/HelpTooltip" -dayjs.extend(relativeTime) - export const Language = { developerCount: (ownerCount: number): string => { return `${ownerCount} developer${ownerCount !== 1 ? "s" : ""}` @@ -151,7 +148,7 @@ export const TemplatesPageView: FC = (props) => { - {dayjs().to(dayjs(template.updated_at))} + {createDayString(template.updated_at)} {template.created_by_name} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index 34199cb5b59de..46b5283a96a59 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -1,5 +1,6 @@ import { screen } from "@testing-library/react" import { rest } from "msw" +import * as CreateDayString from "util/createDayString" import { Language as WorkspacesTableBodyLanguage } from "../../components/WorkspacesTable/WorkspacesTableBody" import { MockWorkspace } from "../../testHelpers/entities" import { history, render } from "../../testHelpers/renderHelpers" @@ -9,6 +10,9 @@ import WorkspacesPage from "./WorkspacesPage" describe("WorkspacesPage", () => { beforeEach(() => { 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 () => { diff --git a/site/src/util/createDayString.ts b/site/src/util/createDayString.ts new file mode 100644 index 0000000000000..5aea9856452ce --- /dev/null +++ b/site/src/util/createDayString.ts @@ -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)) +} 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