mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: refactor frontend to use workspace status directly (#4361)
* Add/update copy * Update mocks * Handle disabled button labels separately * Use workspace status directly, use i18n * Update stories and tests * Fix optimistic update in xservice to use status, pending * Rename started to running in story * Fix deletion banner conditional * Send label to disabled button * Refactor workspace actions
This commit is contained in:
@@ -7,72 +7,63 @@ import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
|
||||
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
|
||||
import { LoadingButton } from "components/LoadingButton/LoadingButton"
|
||||
import { FC } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { combineClasses } from "util/combineClasses"
|
||||
import { WorkspaceStateEnum } from "util/workspace"
|
||||
import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton"
|
||||
|
||||
export const Language = {
|
||||
start: "Start",
|
||||
stop: "Stop",
|
||||
delete: "Delete",
|
||||
cancel: "Cancel",
|
||||
update: "Update",
|
||||
updating: "Updating",
|
||||
// these labels are used in WorkspaceActions.tsx
|
||||
starting: "Starting...",
|
||||
stopping: "Stopping...",
|
||||
deleting: "Deleting...",
|
||||
}
|
||||
|
||||
interface WorkspaceAction {
|
||||
handleAction: () => void
|
||||
}
|
||||
|
||||
export const UpdateButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<Button className={styles.actionButton} startIcon={<CloudQueueIcon />} onClick={handleAction}>
|
||||
{Language.update}
|
||||
{t("actionButton.update")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<WorkspaceActionButton
|
||||
className={styles.actionButton}
|
||||
icon={<PlayCircleOutlineIcon />}
|
||||
onClick={handleAction}
|
||||
label={Language.start}
|
||||
label={t("actionButton.start")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const StopButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<WorkspaceActionButton
|
||||
className={styles.actionButton}
|
||||
icon={<CropSquareIcon />}
|
||||
onClick={handleAction}
|
||||
label={Language.stop}
|
||||
label={t("actionButton.stop")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const DeleteButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("workspacePage")
|
||||
|
||||
return (
|
||||
<WorkspaceActionButton
|
||||
className={styles.actionButton}
|
||||
icon={<DeleteOutlineIcon />}
|
||||
onClick={handleAction}
|
||||
label={Language.delete}
|
||||
label={t("actionButton.delete")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -92,15 +83,15 @@ export const CancelButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ han
|
||||
}
|
||||
|
||||
interface DisabledProps {
|
||||
workspaceState: WorkspaceStateEnum
|
||||
label: string
|
||||
}
|
||||
|
||||
export const DisabledButton: FC<React.PropsWithChildren<DisabledProps>> = ({ workspaceState }) => {
|
||||
export const DisabledButton: FC<React.PropsWithChildren<DisabledProps>> = ({ label }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Button disabled className={styles.actionButton}>
|
||||
{workspaceState}
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import { WorkspaceStateEnum } from "util/workspace"
|
||||
import { DeleteButton, DisabledButton, StartButton, UpdateButton } from "./ActionCtas"
|
||||
import { DropdownButton, DropdownButtonProps } from "./DropdownButton"
|
||||
|
||||
@@ -23,7 +22,7 @@ WithDropdown.args = {
|
||||
|
||||
export const WithCancel = Template.bind({})
|
||||
WithCancel.args = {
|
||||
primaryAction: <DisabledButton workspaceState={WorkspaceStateEnum.deleting} />,
|
||||
primaryAction: <DisabledButton workspaceStatus="deleting" />,
|
||||
secondaryActions: [],
|
||||
canCancel: true,
|
||||
handleCancel: action("cancel"),
|
||||
|
||||
@@ -13,8 +13,8 @@ export default {
|
||||
|
||||
const Template: Story<WorkspaceProps> = (args) => <Workspace {...args} />
|
||||
|
||||
export const Started = Template.bind({})
|
||||
Started.args = {
|
||||
export const Running = Template.bind({})
|
||||
Running.args = {
|
||||
bannerProps: {
|
||||
isLoading: false,
|
||||
onExtend: action("extend"),
|
||||
@@ -53,42 +53,32 @@ Started.args = {
|
||||
|
||||
export const WithoutUpdateAccess = Template.bind({})
|
||||
WithoutUpdateAccess.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
canUpdateWorkspace: false,
|
||||
}
|
||||
|
||||
export const Starting = Template.bind({})
|
||||
Starting.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspace: Mocks.MockStartingWorkspace,
|
||||
}
|
||||
|
||||
export const Stopped = Template.bind({})
|
||||
Stopped.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspace: Mocks.MockStoppedWorkspace,
|
||||
}
|
||||
|
||||
export const Stopping = Template.bind({})
|
||||
Stopping.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspace: Mocks.MockStoppingWorkspace,
|
||||
}
|
||||
|
||||
export const Error = Template.bind({})
|
||||
Error.args = {
|
||||
...Started.args,
|
||||
workspace: {
|
||||
...Mocks.MockFailedWorkspace,
|
||||
latest_build: {
|
||||
...Mocks.MockWorkspaceBuild,
|
||||
job: {
|
||||
...Mocks.MockProvisionerJob,
|
||||
status: "failed",
|
||||
},
|
||||
transition: "start",
|
||||
},
|
||||
},
|
||||
export const Failed = Template.bind({})
|
||||
Failed.args = {
|
||||
...Running.args,
|
||||
workspace: Mocks.MockFailedWorkspace,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.BUILD_ERROR]: Mocks.makeMockApiError({
|
||||
message: "A workspace build is already active.",
|
||||
@@ -98,37 +88,37 @@ Error.args = {
|
||||
|
||||
export const Deleting = Template.bind({})
|
||||
Deleting.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspace: Mocks.MockDeletingWorkspace,
|
||||
}
|
||||
|
||||
export const Deleted = Template.bind({})
|
||||
Deleted.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspace: Mocks.MockDeletedWorkspace,
|
||||
}
|
||||
|
||||
export const Canceling = Template.bind({})
|
||||
Canceling.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspace: Mocks.MockCancelingWorkspace,
|
||||
}
|
||||
|
||||
export const Canceled = Template.bind({})
|
||||
Canceled.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspace: Mocks.MockCanceledWorkspace,
|
||||
}
|
||||
|
||||
export const Outdated = Template.bind({})
|
||||
Outdated.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspace: Mocks.MockOutdatedWorkspace,
|
||||
}
|
||||
|
||||
export const GetBuildsError = Template.bind({})
|
||||
GetBuildsError.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.GET_BUILDS_ERROR]: Mocks.makeMockApiError({
|
||||
message: "There is a problem fetching builds.",
|
||||
@@ -138,7 +128,7 @@ GetBuildsError.args = {
|
||||
|
||||
export const GetResourcesError = Template.bind({})
|
||||
GetResourcesError.args = {
|
||||
...Started.args,
|
||||
...Running.args,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.GET_RESOURCES_ERROR]: Mocks.makeMockApiError({
|
||||
message: "There is a problem fetching workspace resources.",
|
||||
@@ -148,7 +138,7 @@ GetResourcesError.args = {
|
||||
|
||||
export const CancellationError = Template.bind({})
|
||||
CancellationError.args = {
|
||||
...Error.args,
|
||||
...Failed.args,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.CANCELLATION_ERROR]: Mocks.makeMockApiError({
|
||||
message: "Job could not be canceled.",
|
||||
|
||||
@@ -100,7 +100,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
/>
|
||||
<WorkspaceActions
|
||||
workspace={workspace}
|
||||
workspaceStatus={workspace.latest_build.status}
|
||||
isOutdated={workspace.outdated}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleDelete={handleDelete}
|
||||
|
||||
@@ -16,72 +16,74 @@ const defaultArgs = {
|
||||
handleDelete: action("delete"),
|
||||
handleUpdate: action("update"),
|
||||
handleCancel: action("cancel"),
|
||||
isOutdated: false,
|
||||
isUpdating: false,
|
||||
}
|
||||
|
||||
export const Starting = Template.bind({})
|
||||
Starting.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockStartingWorkspace,
|
||||
workspaceStatus: Mocks.MockStartingWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Started = Template.bind({})
|
||||
Started.args = {
|
||||
export const Running = Template.bind({})
|
||||
Running.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockWorkspace,
|
||||
workspaceStatus: Mocks.MockWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Stopping = Template.bind({})
|
||||
Stopping.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockStoppingWorkspace,
|
||||
workspaceStatus: Mocks.MockStoppingWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Stopped = Template.bind({})
|
||||
Stopped.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockStoppedWorkspace,
|
||||
workspaceStatus: Mocks.MockStoppedWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Canceling = Template.bind({})
|
||||
Canceling.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockCancelingWorkspace,
|
||||
workspaceStatus: Mocks.MockCancelingWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Canceled = Template.bind({})
|
||||
Canceled.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockCanceledWorkspace,
|
||||
workspaceStatus: Mocks.MockCanceledWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Deleting = Template.bind({})
|
||||
Deleting.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockDeletingWorkspace,
|
||||
workspaceStatus: Mocks.MockDeletingWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Deleted = Template.bind({})
|
||||
Deleted.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockDeletedWorkspace,
|
||||
workspaceStatus: Mocks.MockDeletedWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Outdated = Template.bind({})
|
||||
Outdated.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockOutdatedWorkspace,
|
||||
isOutdated: true,
|
||||
workspaceStatus: Mocks.MockOutdatedWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Errored = Template.bind({})
|
||||
Errored.args = {
|
||||
export const Failed = Template.bind({})
|
||||
Failed.args = {
|
||||
...defaultArgs,
|
||||
workspace: Mocks.MockFailedWorkspace,
|
||||
workspaceStatus: Mocks.MockFailedWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
export const Updating = Template.bind({})
|
||||
Updating.args = {
|
||||
...defaultArgs,
|
||||
isUpdating: true,
|
||||
workspace: Mocks.MockOutdatedWorkspace,
|
||||
workspaceStatus: Mocks.MockOutdatedWorkspace.latest_build.status,
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { fireEvent, screen } from "@testing-library/react"
|
||||
import { WorkspaceStateEnum } from "util/workspace"
|
||||
import i18next from "i18next"
|
||||
import * as Mocks from "../../testHelpers/entities"
|
||||
import { render } from "../../testHelpers/renderHelpers"
|
||||
import { Language } from "../DropdownButton/ActionCtas"
|
||||
import { WorkspaceActions, WorkspaceActionsProps } from "./WorkspaceActions"
|
||||
|
||||
const { t } = i18next
|
||||
|
||||
const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
||||
render(
|
||||
<WorkspaceActions
|
||||
workspace={props.workspace ?? Mocks.MockWorkspace}
|
||||
workspaceStatus={props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status}
|
||||
isOutdated={props.isOutdated ?? false}
|
||||
handleStart={jest.fn()}
|
||||
handleStop={jest.fn()}
|
||||
handleDelete={jest.fn()}
|
||||
@@ -22,7 +24,8 @@ const renderComponent = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
||||
const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
||||
render(
|
||||
<WorkspaceActions
|
||||
workspace={props.workspace ?? Mocks.MockWorkspace}
|
||||
workspaceStatus={props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status}
|
||||
isOutdated={props.isOutdated ?? false}
|
||||
handleStart={jest.fn()}
|
||||
handleStop={jest.fn()}
|
||||
handleDelete={jest.fn()}
|
||||
@@ -38,8 +41,10 @@ const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
|
||||
describe("WorkspaceActions", () => {
|
||||
describe("when the workspace is starting", () => {
|
||||
it("primary is starting; cancel is available; no secondary", async () => {
|
||||
await renderComponent({ workspace: Mocks.MockStartingWorkspace })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.starting)
|
||||
await renderComponent({ workspaceStatus: Mocks.MockStartingWorkspace.latest_build.status })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("actionButton.starting", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(
|
||||
screen.getByRole("button", {
|
||||
name: "cancel action",
|
||||
@@ -50,15 +55,21 @@ describe("WorkspaceActions", () => {
|
||||
})
|
||||
describe("when the workspace is started", () => {
|
||||
it("primary is stop; secondary is delete", async () => {
|
||||
await renderAndClick({ workspace: Mocks.MockWorkspace })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.stop)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
|
||||
await renderAndClick({ workspaceStatus: Mocks.MockWorkspace.latest_build.status })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("actionButton.stop", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(
|
||||
t("actionButton.delete", { ns: "workspacePage" }),
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("when the workspace is stopping", () => {
|
||||
it("primary is stopping; cancel is available; no secondary", async () => {
|
||||
await renderComponent({ workspace: Mocks.MockStoppingWorkspace })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.stopping)
|
||||
await renderComponent({ workspaceStatus: Mocks.MockStoppingWorkspace.latest_build.status })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("actionButton.stopping", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(
|
||||
screen.getByRole("button", {
|
||||
name: "cancel action",
|
||||
@@ -69,30 +80,44 @@ describe("WorkspaceActions", () => {
|
||||
})
|
||||
describe("when the workspace is canceling", () => {
|
||||
it("primary is canceling; no secondary", async () => {
|
||||
await renderAndClick({ workspace: Mocks.MockCancelingWorkspace })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(WorkspaceStateEnum.canceling)
|
||||
await renderAndClick({ workspaceStatus: Mocks.MockCancelingWorkspace.latest_build.status })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("disabledButton.canceling", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.queryByTestId("secondary-ctas")).toBeNull()
|
||||
})
|
||||
})
|
||||
describe("when the workspace is canceled", () => {
|
||||
it("primary is start; secondary are stop, delete", async () => {
|
||||
await renderAndClick({ workspace: Mocks.MockCanceledWorkspace })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.start)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.stop)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
|
||||
await renderAndClick({ workspaceStatus: Mocks.MockCanceledWorkspace.latest_build.status })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("actionButton.start", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(
|
||||
t("actionButton.stop", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(
|
||||
t("actionButton.delete", { ns: "workspacePage" }),
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("when the workspace is errored", () => {
|
||||
it("primary is start; secondary is delete", async () => {
|
||||
await renderAndClick({ workspace: Mocks.MockFailedWorkspace })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.start)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
|
||||
await renderAndClick({ workspaceStatus: Mocks.MockFailedWorkspace.latest_build.status })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("actionButton.start", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(
|
||||
t("actionButton.delete", { ns: "workspacePage" }),
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("when the workspace is deleting", () => {
|
||||
it("primary is deleting; cancel is available; no secondary", async () => {
|
||||
await renderComponent({ workspace: Mocks.MockDeletingWorkspace })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.deleting)
|
||||
await renderComponent({ workspaceStatus: Mocks.MockDeletingWorkspace.latest_build.status })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("actionButton.deleting", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(
|
||||
screen.getByRole("button", {
|
||||
name: "cancel action",
|
||||
@@ -103,17 +128,28 @@ describe("WorkspaceActions", () => {
|
||||
})
|
||||
describe("when the workspace is deleted", () => {
|
||||
it("primary is deleted; no secondary", async () => {
|
||||
await renderAndClick({ workspace: Mocks.MockDeletedWorkspace })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(WorkspaceStateEnum.deleted)
|
||||
await renderAndClick({ workspaceStatus: Mocks.MockDeletedWorkspace.latest_build.status })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("disabledButton.deleted", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.queryByTestId("secondary-ctas")).toBeNull()
|
||||
})
|
||||
})
|
||||
describe("when the workspace is outdated", () => {
|
||||
it("primary is update; secondary are start, delete", async () => {
|
||||
await renderAndClick({ workspace: Mocks.MockOutdatedWorkspace })
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.update)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.start)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
|
||||
await renderAndClick({
|
||||
isOutdated: true,
|
||||
workspaceStatus: Mocks.MockOutdatedWorkspace.latest_build.status,
|
||||
})
|
||||
expect(screen.getByTestId("primary-cta")).toHaveTextContent(
|
||||
t("actionButton.update", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(
|
||||
t("actionButton.start", { ns: "workspacePage" }),
|
||||
)
|
||||
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(
|
||||
t("actionButton.delete", { ns: "workspacePage" }),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import { DropdownButton } from "components/DropdownButton/DropdownButton"
|
||||
import { FC, ReactNode, useMemo } from "react"
|
||||
import { getWorkspaceStatus, WorkspaceStateEnum, WorkspaceStatus } from "util/workspace"
|
||||
import { Workspace } from "../../api/typesGenerated"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { WorkspaceStatus } from "../../api/typesGenerated"
|
||||
import {
|
||||
ActionLoadingButton,
|
||||
DeleteButton,
|
||||
DisabledButton,
|
||||
Language,
|
||||
StartButton,
|
||||
StopButton,
|
||||
UpdateButton,
|
||||
} from "../DropdownButton/ActionCtas"
|
||||
import { ButtonMapping, ButtonTypesEnum, WorkspaceStateActions } 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)
|
||||
import { ButtonMapping, ButtonTypesEnum, statusToAbilities } from "./constants"
|
||||
|
||||
export interface WorkspaceActionsProps {
|
||||
workspace: Workspace
|
||||
workspaceStatus: WorkspaceStatus
|
||||
isOutdated: boolean
|
||||
handleStart: () => void
|
||||
handleStop: () => void
|
||||
handleDelete: () => void
|
||||
@@ -32,7 +25,8 @@ export interface WorkspaceActionsProps {
|
||||
}
|
||||
|
||||
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
workspace,
|
||||
workspaceStatus,
|
||||
isOutdated,
|
||||
handleStart,
|
||||
handleStop,
|
||||
handleDelete,
|
||||
@@ -40,52 +34,42 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
handleCancel,
|
||||
isUpdating,
|
||||
}) => {
|
||||
const workspaceStatus: keyof typeof WorkspaceStateEnum = getWorkspaceStatus(
|
||||
workspace.latest_build,
|
||||
)
|
||||
const workspaceState = WorkspaceStateEnum[workspaceStatus]
|
||||
|
||||
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 = [updatedActions.primary, ...updatedActions.secondary]
|
||||
updatedActions.primary = ButtonTypesEnum.update
|
||||
|
||||
return updatedActions
|
||||
}, [canBeUpdated, workspaceState])
|
||||
const { t } = useTranslation("workspacePage")
|
||||
const { canCancel, canAcceptJobs, actions } = statusToAbilities[workspaceStatus]
|
||||
const canBeUpdated = isOutdated && canAcceptJobs
|
||||
|
||||
// A mapping of button type to the corresponding React component
|
||||
const buttonMapping: ButtonMapping = {
|
||||
[ButtonTypesEnum.update]: <UpdateButton handleAction={handleUpdate} />,
|
||||
[ButtonTypesEnum.updating]: <ActionLoadingButton label={Language.updating} />,
|
||||
[ButtonTypesEnum.updating]: <ActionLoadingButton label={t("actionButton.updating")} />,
|
||||
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
|
||||
[ButtonTypesEnum.starting]: <ActionLoadingButton label={Language.starting} />,
|
||||
[ButtonTypesEnum.starting]: <ActionLoadingButton label={t("actionButton.starting")} />,
|
||||
[ButtonTypesEnum.stop]: <StopButton handleAction={handleStop} />,
|
||||
[ButtonTypesEnum.stopping]: <ActionLoadingButton label={Language.stopping} />,
|
||||
[ButtonTypesEnum.stopping]: <ActionLoadingButton label={t("actionButton.stopping")} />,
|
||||
[ButtonTypesEnum.delete]: <DeleteButton handleAction={handleDelete} />,
|
||||
[ButtonTypesEnum.deleting]: <ActionLoadingButton label={Language.deleting} />,
|
||||
[ButtonTypesEnum.canceling]: <DisabledButton workspaceState={workspaceState} />,
|
||||
[ButtonTypesEnum.disabled]: <DisabledButton workspaceState={workspaceState} />,
|
||||
[ButtonTypesEnum.queued]: <DisabledButton workspaceState={workspaceState} />,
|
||||
[ButtonTypesEnum.loading]: <DisabledButton workspaceState={workspaceState} />,
|
||||
[ButtonTypesEnum.deleting]: <ActionLoadingButton label={t("actionButton.deleting")} />,
|
||||
[ButtonTypesEnum.canceling]: <DisabledButton label={t("disabledButton.canceling")} />,
|
||||
[ButtonTypesEnum.deleted]: <DisabledButton label={t("disabledButton.deleted")} />,
|
||||
[ButtonTypesEnum.pending]: <DisabledButton label={t("disabledButton.pending")} />,
|
||||
}
|
||||
|
||||
// memoize so this isn't recalculated every time we fetch the workspace
|
||||
const [primaryAction, ...secondaryActions] = useMemo(
|
||||
() =>
|
||||
isUpdating
|
||||
? [ButtonTypesEnum.updating, ...actions]
|
||||
: canBeUpdated
|
||||
? [ButtonTypesEnum.update, ...actions]
|
||||
: actions,
|
||||
[actions, canBeUpdated, isUpdating],
|
||||
)
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
primaryAction={
|
||||
isUpdating ? buttonMapping[ButtonTypesEnum.updating] : buttonMapping[actions.primary]
|
||||
}
|
||||
canCancel={actions.canCancel}
|
||||
primaryAction={buttonMapping[primaryAction]}
|
||||
canCancel={canCancel}
|
||||
handleCancel={handleCancel}
|
||||
secondaryActions={actions.secondary.map((action) => ({
|
||||
secondaryActions={secondaryActions.map((action) => ({
|
||||
action,
|
||||
button: buttonMapping[action],
|
||||
}))}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WorkspaceStatus } from "api/typesGenerated"
|
||||
import { ReactNode } from "react"
|
||||
import { WorkspaceStateEnum } from "util/workspace"
|
||||
|
||||
// the button types we have
|
||||
export enum ButtonTypesEnum {
|
||||
@@ -13,84 +13,73 @@ export enum ButtonTypesEnum {
|
||||
updating = "updating",
|
||||
// disabled buttons
|
||||
canceling = "canceling",
|
||||
disabled = "disabled",
|
||||
queued = "queued",
|
||||
loading = "loading",
|
||||
deleted = "deleted",
|
||||
pending = "pending",
|
||||
}
|
||||
|
||||
export type ButtonMapping = {
|
||||
[key in ButtonTypesEnum]: ReactNode
|
||||
}
|
||||
|
||||
type StateActionsType = {
|
||||
[key in WorkspaceStateEnum]: {
|
||||
primary: ButtonTypesEnum
|
||||
secondary: ButtonTypesEnum[]
|
||||
canCancel: boolean
|
||||
}
|
||||
interface WorkspaceAbilities {
|
||||
actions: ButtonTypesEnum[]
|
||||
canCancel: boolean
|
||||
canAcceptJobs: boolean
|
||||
}
|
||||
|
||||
// A mapping of workspace state to button type
|
||||
// 'Primary' actions are the main ctas
|
||||
// 'Secondary' actions are ctas housed within the popover
|
||||
export const WorkspaceStateActions: StateActionsType = {
|
||||
[WorkspaceStateEnum.starting]: {
|
||||
primary: ButtonTypesEnum.starting,
|
||||
secondary: [],
|
||||
export const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
|
||||
starting: {
|
||||
actions: [ButtonTypesEnum.starting],
|
||||
canCancel: true,
|
||||
canAcceptJobs: false,
|
||||
},
|
||||
[WorkspaceStateEnum.started]: {
|
||||
primary: ButtonTypesEnum.stop,
|
||||
secondary: [ButtonTypesEnum.delete],
|
||||
running: {
|
||||
actions: [ButtonTypesEnum.stop, ButtonTypesEnum.delete],
|
||||
canCancel: false,
|
||||
canAcceptJobs: true,
|
||||
},
|
||||
[WorkspaceStateEnum.stopping]: {
|
||||
primary: ButtonTypesEnum.stopping,
|
||||
secondary: [],
|
||||
stopping: {
|
||||
actions: [ButtonTypesEnum.stopping],
|
||||
canCancel: true,
|
||||
canAcceptJobs: false,
|
||||
},
|
||||
[WorkspaceStateEnum.stopped]: {
|
||||
primary: ButtonTypesEnum.start,
|
||||
secondary: [ButtonTypesEnum.delete],
|
||||
stopped: {
|
||||
actions: [ButtonTypesEnum.start, ButtonTypesEnum.delete],
|
||||
canCancel: false,
|
||||
canAcceptJobs: true,
|
||||
},
|
||||
[WorkspaceStateEnum.canceled]: {
|
||||
primary: ButtonTypesEnum.start,
|
||||
secondary: [ButtonTypesEnum.stop, ButtonTypesEnum.delete],
|
||||
canceled: {
|
||||
actions: [ButtonTypesEnum.start, ButtonTypesEnum.stop, ButtonTypesEnum.delete],
|
||||
canCancel: false,
|
||||
canAcceptJobs: true,
|
||||
},
|
||||
// in the case of an error
|
||||
[WorkspaceStateEnum.error]: {
|
||||
primary: ButtonTypesEnum.start, // give the user the ability to start a workspace again
|
||||
secondary: [ButtonTypesEnum.delete], // allows the user to delete
|
||||
failed: {
|
||||
actions: [ButtonTypesEnum.start, ButtonTypesEnum.delete],
|
||||
canCancel: false,
|
||||
canAcceptJobs: true,
|
||||
},
|
||||
/**
|
||||
* disabled states
|
||||
*/
|
||||
[WorkspaceStateEnum.canceling]: {
|
||||
primary: ButtonTypesEnum.canceling,
|
||||
secondary: [],
|
||||
canceling: {
|
||||
actions: [ButtonTypesEnum.canceling],
|
||||
canCancel: false,
|
||||
canAcceptJobs: false,
|
||||
},
|
||||
[WorkspaceStateEnum.deleting]: {
|
||||
primary: ButtonTypesEnum.deleting,
|
||||
secondary: [],
|
||||
deleting: {
|
||||
actions: [ButtonTypesEnum.deleting],
|
||||
canCancel: true,
|
||||
canAcceptJobs: false,
|
||||
},
|
||||
[WorkspaceStateEnum.deleted]: {
|
||||
primary: ButtonTypesEnum.disabled,
|
||||
secondary: [],
|
||||
deleted: {
|
||||
actions: [ButtonTypesEnum.deleted],
|
||||
canCancel: false,
|
||||
canAcceptJobs: true,
|
||||
},
|
||||
[WorkspaceStateEnum.queued]: {
|
||||
primary: ButtonTypesEnum.queued,
|
||||
secondary: [],
|
||||
canCancel: false,
|
||||
},
|
||||
[WorkspaceStateEnum.loading]: {
|
||||
primary: ButtonTypesEnum.loading,
|
||||
secondary: [],
|
||||
pending: {
|
||||
actions: [ButtonTypesEnum.pending],
|
||||
canCancel: false,
|
||||
canAcceptJobs: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import Button from "@material-ui/core/Button"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import Alert from "@material-ui/lab/Alert"
|
||||
import AlertTitle from "@material-ui/lab/AlertTitle"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
import { FC } from "react"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { isWorkspaceDeleted } from "../../util/workspace"
|
||||
|
||||
const Language = {
|
||||
bannerTitle: "This workspace has been deleted and cannot be edited.",
|
||||
@@ -22,22 +22,20 @@ export const WorkspaceDeletedBanner: FC<React.PropsWithChildren<WorkspaceDeleted
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
if (!isWorkspaceDeleted(workspace)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
className={styles.root}
|
||||
action={
|
||||
<Button color="inherit" onClick={handleClick} size="small">
|
||||
{Language.createWorkspaceCta}
|
||||
</Button>
|
||||
}
|
||||
severity="warning"
|
||||
>
|
||||
<AlertTitle>{Language.bannerTitle}</AlertTitle>
|
||||
</Alert>
|
||||
<Maybe condition={workspace.latest_build.status === "deleted"}>
|
||||
<Alert
|
||||
className={styles.root}
|
||||
action={
|
||||
<Button color="inherit" onClick={handleClick} size="small">
|
||||
{Language.createWorkspaceCta}
|
||||
</Button>
|
||||
}
|
||||
severity="warning"
|
||||
>
|
||||
<AlertTitle>{Language.bannerTitle}</AlertTitle>
|
||||
</Alert>
|
||||
</Maybe>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
MockDeletedWorkspace,
|
||||
MockDeletingWorkspace,
|
||||
MockFailedWorkspace,
|
||||
MockQueuedWorkspace,
|
||||
MockPendingWorkspace,
|
||||
MockStartingWorkspace,
|
||||
MockStoppedWorkspace,
|
||||
MockStoppingWorkspace,
|
||||
@@ -65,7 +65,7 @@ Failed.args = {
|
||||
build: MockFailedWorkspace.latest_build,
|
||||
}
|
||||
|
||||
export const Queued = Template.bind({})
|
||||
Queued.args = {
|
||||
build: MockQueuedWorkspace.latest_build,
|
||||
export const Pending = Template.bind({})
|
||||
Pending.args = {
|
||||
build: MockPendingWorkspace.latest_build,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Pill } from "components/Pill/Pill"
|
||||
import i18next from "i18next"
|
||||
import React from "react"
|
||||
import { PaletteIndex } from "theme/palettes"
|
||||
import { getWorkspaceStatus } from "util/workspace"
|
||||
|
||||
const LoadingIcon: React.FC = () => {
|
||||
return <CircularProgress size={10} style={{ color: "#FFF" }} />
|
||||
@@ -20,19 +19,18 @@ export const getStatus = (
|
||||
text: string
|
||||
icon: React.ReactNode
|
||||
} => {
|
||||
const status = getWorkspaceStatus(build)
|
||||
const { t } = i18next
|
||||
|
||||
switch (status) {
|
||||
switch (build.status) {
|
||||
case undefined:
|
||||
return {
|
||||
text: t("workspaceStatus.loading", { ns: "common" }),
|
||||
icon: <LoadingIcon />,
|
||||
}
|
||||
case "started":
|
||||
case "running":
|
||||
return {
|
||||
type: "success",
|
||||
text: t("workspaceStatus.started", { ns: "common" }),
|
||||
text: t("workspaceStatus.running", { ns: "common" }),
|
||||
icon: <PlayIcon />,
|
||||
}
|
||||
case "starting":
|
||||
@@ -77,16 +75,16 @@ export const getStatus = (
|
||||
text: t("workspaceStatus.canceled", { ns: "common" }),
|
||||
icon: <ErrorIcon />,
|
||||
}
|
||||
case "error":
|
||||
case "failed":
|
||||
return {
|
||||
type: "error",
|
||||
text: t("workspaceStatus.failed", { ns: "common" }),
|
||||
icon: <ErrorIcon />,
|
||||
}
|
||||
case "queued":
|
||||
case "pending":
|
||||
return {
|
||||
type: "info",
|
||||
text: t("workspaceStatus.queued", { ns: "common" }),
|
||||
text: t("workspaceStatus.pending", { ns: "common" }),
|
||||
icon: <LoadingIcon />,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"workspaceStatus": {
|
||||
"loading": "Loading",
|
||||
"started": "Running",
|
||||
"running": "Running",
|
||||
"starting": "Starting",
|
||||
"stopping": "Stopping",
|
||||
"stopped": "Stopped",
|
||||
@@ -10,7 +10,7 @@
|
||||
"canceling": "Canceling action",
|
||||
"canceled": "Canceled action",
|
||||
"failed": "Failed",
|
||||
"queued": "Queued"
|
||||
"pending": "Pending"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Delete {{entity}}",
|
||||
|
||||
@@ -6,5 +6,21 @@
|
||||
"schedule": "Schedule",
|
||||
"editDeadlineMinus": "Subtract one hour",
|
||||
"editDeadlinePlus": "Add one hour"
|
||||
},
|
||||
"actionButton": {
|
||||
"start": "Start",
|
||||
"stop": "Stop",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"update": "Update",
|
||||
"updating": "Updating",
|
||||
"starting": "Starting...",
|
||||
"stopping": "Stopping...",
|
||||
"deleting": "Deleting..."
|
||||
},
|
||||
"disabledButton": {
|
||||
"canceling": "Canceling",
|
||||
"deleted": "Deleted",
|
||||
"pending": "Pending"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import i18next from "i18next"
|
||||
import { rest } from "msw"
|
||||
import * as api from "../../api/api"
|
||||
import { Workspace } from "../../api/typesGenerated"
|
||||
import { Language } from "../../components/DropdownButton/ActionCtas"
|
||||
import {
|
||||
MockBuilds,
|
||||
MockCanceledWorkspace,
|
||||
@@ -28,7 +27,7 @@ import {
|
||||
renderWithAuth,
|
||||
} from "../../testHelpers/renderHelpers"
|
||||
import { server } from "../../testHelpers/server"
|
||||
import { DisplayAgentStatusLanguage, DisplayStatusLanguage } from "../../util/workspace"
|
||||
import { DisplayAgentStatusLanguage } from "../../util/workspace"
|
||||
import { WorkspacePage } from "./WorkspacePage"
|
||||
|
||||
const { t } = i18next
|
||||
@@ -94,7 +93,7 @@ describe("WorkspacePage", () => {
|
||||
const stopWorkspaceMock = jest
|
||||
.spyOn(api, "stopWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild)
|
||||
testButton(Language.stop, stopWorkspaceMock)
|
||||
testButton(t("actionButton.stop", { ns: "workspacePage" }), stopWorkspaceMock)
|
||||
})
|
||||
|
||||
it("requests a delete job when the user presses Delete and confirms", async () => {
|
||||
@@ -108,7 +107,8 @@ describe("WorkspacePage", () => {
|
||||
const trigger = await screen.findByTestId("workspace-actions-button")
|
||||
await user.click(trigger)
|
||||
|
||||
const button = await screen.findByText(Language.delete)
|
||||
const buttonText = t("actionButton.delete", { ns: "workspacePage" })
|
||||
const button = await screen.findByText(buttonText)
|
||||
await user.click(button)
|
||||
|
||||
const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "workspace" })
|
||||
@@ -128,7 +128,7 @@ describe("WorkspacePage", () => {
|
||||
const startWorkspaceMock = jest
|
||||
.spyOn(api, "startWorkspace")
|
||||
.mockImplementation(() => Promise.resolve(MockWorkspaceBuild))
|
||||
testButton(Language.start, startWorkspaceMock)
|
||||
testButton(t("actionButton.start", { ns: "workspacePage" }), startWorkspaceMock)
|
||||
})
|
||||
it("requests cancellation when the user presses Cancel", async () => {
|
||||
server.use(
|
||||
@@ -159,7 +159,8 @@ describe("WorkspacePage", () => {
|
||||
)
|
||||
|
||||
await renderWorkspacePage()
|
||||
const button = await screen.findByText(Language.update, { exact: true })
|
||||
const buttonText = t("actionButton.update", { ns: "workspacePage" })
|
||||
const button = await screen.findByText(buttonText, { exact: true })
|
||||
fireEvent.click(button)
|
||||
|
||||
// getTemplate is called twice: once when the machine starts, and once after the user requests to update
|
||||
@@ -177,7 +178,8 @@ describe("WorkspacePage", () => {
|
||||
}),
|
||||
)
|
||||
await renderWorkspacePage()
|
||||
const button = await screen.findByText(Language.update, { exact: true })
|
||||
const buttonText = t("actionButton.update", { ns: "workspacePage" })
|
||||
const button = await screen.findByText(buttonText, { exact: true })
|
||||
fireEvent.click(button)
|
||||
|
||||
await waitFor(() =>
|
||||
@@ -185,28 +187,28 @@ describe("WorkspacePage", () => {
|
||||
)
|
||||
})
|
||||
it("shows the Stopping status when the workspace is stopping", async () => {
|
||||
await testStatus(MockStoppingWorkspace, DisplayStatusLanguage.stopping)
|
||||
await testStatus(MockStoppingWorkspace, t("workspaceStatus.stopping", { ns: "common" }))
|
||||
})
|
||||
it("shows the Stopped status when the workspace is stopped", async () => {
|
||||
await testStatus(MockStoppedWorkspace, DisplayStatusLanguage.stopped)
|
||||
await testStatus(MockStoppedWorkspace, t("workspaceStatus.stopped", { ns: "common" }))
|
||||
})
|
||||
it("shows the Building status when the workspace is starting", async () => {
|
||||
await testStatus(MockStartingWorkspace, DisplayStatusLanguage.starting)
|
||||
await testStatus(MockStartingWorkspace, t("workspaceStatus.starting", { ns: "common" }))
|
||||
})
|
||||
it("shows the Running status when the workspace is started", async () => {
|
||||
await testStatus(MockWorkspace, DisplayStatusLanguage.started)
|
||||
it("shows the Running status when the workspace is running", async () => {
|
||||
await testStatus(MockWorkspace, t("workspaceStatus.running", { ns: "common" }))
|
||||
})
|
||||
it("shows the Failed status when the workspace is failed or canceled", async () => {
|
||||
await testStatus(MockFailedWorkspace, DisplayStatusLanguage.failed)
|
||||
await testStatus(MockFailedWorkspace, t("workspaceStatus.failed", { ns: "common" }))
|
||||
})
|
||||
it("shows the Canceling status when the workspace is canceling", async () => {
|
||||
await testStatus(MockCancelingWorkspace, DisplayStatusLanguage.canceling)
|
||||
await testStatus(MockCancelingWorkspace, t("workspaceStatus.canceling", { ns: "common" }))
|
||||
})
|
||||
it("shows the Canceled status when the workspace is canceling", async () => {
|
||||
await testStatus(MockCanceledWorkspace, DisplayStatusLanguage.canceled)
|
||||
await testStatus(MockCanceledWorkspace, t("workspaceStatus.canceled", { ns: "common" }))
|
||||
})
|
||||
it("shows the Deleting status when the workspace is deleting", async () => {
|
||||
await testStatus(MockDeletingWorkspace, DisplayStatusLanguage.deleting)
|
||||
await testStatus(MockDeletingWorkspace, t("workspaceStatus.deleting", { ns: "common" }))
|
||||
})
|
||||
it("shows the Deleted status when the workspace is deleted", async () => {
|
||||
await testStatus(MockDeletedWorkspace, t("workspaceStatus.deleted", { ns: "common" }))
|
||||
|
||||
@@ -396,13 +396,14 @@ export const MockWorkspace: TypesGen.Workspace = {
|
||||
|
||||
export const MockStoppedWorkspace: TypesGen.Workspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: MockWorkspaceBuildStop,
|
||||
latest_build: { ...MockWorkspaceBuildStop, status: "stopped" },
|
||||
}
|
||||
export const MockStoppingWorkspace: TypesGen.Workspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: {
|
||||
...MockWorkspaceBuildStop,
|
||||
job: MockRunningProvisionerJob,
|
||||
status: "stopping",
|
||||
},
|
||||
}
|
||||
export const MockStartingWorkspace: TypesGen.Workspace = {
|
||||
@@ -411,40 +412,43 @@ export const MockStartingWorkspace: TypesGen.Workspace = {
|
||||
...MockWorkspaceBuild,
|
||||
job: MockRunningProvisionerJob,
|
||||
transition: "start",
|
||||
status: "starting",
|
||||
},
|
||||
}
|
||||
export const MockCancelingWorkspace: TypesGen.Workspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: { ...MockWorkspaceBuild, job: MockCancelingProvisionerJob },
|
||||
latest_build: { ...MockWorkspaceBuild, job: MockCancelingProvisionerJob, status: "canceling" },
|
||||
}
|
||||
export const MockCanceledWorkspace: TypesGen.Workspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: { ...MockWorkspaceBuild, job: MockCanceledProvisionerJob },
|
||||
latest_build: { ...MockWorkspaceBuild, job: MockCanceledProvisionerJob, status: "canceled" },
|
||||
}
|
||||
export const MockFailedWorkspace: TypesGen.Workspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: {
|
||||
...MockWorkspaceBuild,
|
||||
job: MockFailedProvisionerJob,
|
||||
status: "failed",
|
||||
},
|
||||
}
|
||||
export const MockDeletingWorkspace: TypesGen.Workspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: { ...MockWorkspaceBuildDelete, job: MockRunningProvisionerJob },
|
||||
latest_build: { ...MockWorkspaceBuildDelete, job: MockRunningProvisionerJob, status: "deleting" },
|
||||
}
|
||||
export const MockDeletedWorkspace: TypesGen.Workspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: MockWorkspaceBuildDelete,
|
||||
latest_build: { ...MockWorkspaceBuildDelete, status: "deleted" },
|
||||
}
|
||||
|
||||
export const MockOutdatedWorkspace: TypesGen.Workspace = { ...MockFailedWorkspace, outdated: true }
|
||||
|
||||
export const MockQueuedWorkspace: TypesGen.Workspace = {
|
||||
export const MockPendingWorkspace: TypesGen.Workspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: {
|
||||
...MockWorkspaceBuild,
|
||||
job: MockPendingProvisionerJob,
|
||||
transition: "start",
|
||||
status: "pending",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
defaultWorkspaceExtension,
|
||||
getDisplayVersionStatus,
|
||||
getDisplayWorkspaceBuildInitiatedBy,
|
||||
isWorkspaceDeleted,
|
||||
isWorkspaceOn,
|
||||
} from "./workspace"
|
||||
|
||||
@@ -48,44 +47,6 @@ describe("util > workspace", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("isWorkspaceDeleted", () => {
|
||||
it.each<[TypesGen.WorkspaceTransition, TypesGen.ProvisionerJobStatus, boolean]>([
|
||||
["delete", "canceled", false],
|
||||
["delete", "canceling", false],
|
||||
["delete", "failed", false],
|
||||
["delete", "pending", false],
|
||||
["delete", "running", false],
|
||||
["delete", "succeeded", true],
|
||||
|
||||
["stop", "canceled", false],
|
||||
["stop", "canceling", false],
|
||||
["stop", "failed", false],
|
||||
["stop", "pending", false],
|
||||
["stop", "running", false],
|
||||
["stop", "succeeded", false],
|
||||
|
||||
["start", "canceled", false],
|
||||
["start", "canceling", false],
|
||||
["start", "failed", false],
|
||||
["start", "pending", false],
|
||||
["start", "running", false],
|
||||
["start", "succeeded", false],
|
||||
])(`transition=%p, status=%p, isWorkspaceDeleted=%p`, (transition, status, isDeleted) => {
|
||||
const workspace: TypesGen.Workspace = {
|
||||
...Mocks.MockWorkspace,
|
||||
latest_build: {
|
||||
...Mocks.MockWorkspaceBuild,
|
||||
job: {
|
||||
...Mocks.MockProvisionerJob,
|
||||
status,
|
||||
},
|
||||
transition,
|
||||
},
|
||||
}
|
||||
expect(isWorkspaceDeleted(workspace)).toBe(isDeleted)
|
||||
})
|
||||
})
|
||||
|
||||
describe("defaultWorkspaceExtension", () => {
|
||||
it.each<[string, TypesGen.PutExtendWorkspaceRequest]>([
|
||||
[
|
||||
|
||||
@@ -4,89 +4,12 @@ import duration from "dayjs/plugin/duration"
|
||||
import minMax from "dayjs/plugin/minMax"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import semver from "semver"
|
||||
import { WorkspaceBuildTransition } from "../api/types"
|
||||
import * as TypesGen from "../api/typesGenerated"
|
||||
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(minMax)
|
||||
|
||||
// all the possible states returned by the API
|
||||
export enum WorkspaceStateEnum {
|
||||
starting = "Starting",
|
||||
started = "Started",
|
||||
stopping = "Stopping",
|
||||
stopped = "Stopped",
|
||||
canceling = "Canceling",
|
||||
canceled = "Canceled",
|
||||
deleting = "Deleting",
|
||||
deleted = "Deleted",
|
||||
queued = "Queued",
|
||||
error = "Error",
|
||||
loading = "Loading",
|
||||
}
|
||||
|
||||
export type WorkspaceStatus =
|
||||
| "queued"
|
||||
| "started"
|
||||
| "starting"
|
||||
| "stopped"
|
||||
| "stopping"
|
||||
| "error"
|
||||
| "loading"
|
||||
| "deleting"
|
||||
| "deleted"
|
||||
| "canceled"
|
||||
| "canceling"
|
||||
|
||||
const inProgressToStatus: Record<WorkspaceBuildTransition, WorkspaceStatus> = {
|
||||
start: "starting",
|
||||
stop: "stopping",
|
||||
delete: "deleting",
|
||||
}
|
||||
|
||||
const succeededToStatus: Record<WorkspaceBuildTransition, WorkspaceStatus> = {
|
||||
start: "started",
|
||||
stop: "stopped",
|
||||
delete: "deleted",
|
||||
}
|
||||
|
||||
// Converts a workspaces status to a human-readable form.
|
||||
export const getWorkspaceStatus = (workspaceBuild?: TypesGen.WorkspaceBuild): WorkspaceStatus => {
|
||||
const transition = workspaceBuild?.transition as WorkspaceBuildTransition
|
||||
const jobStatus = workspaceBuild?.job.status
|
||||
switch (jobStatus) {
|
||||
case undefined:
|
||||
return "loading"
|
||||
case "succeeded":
|
||||
return succeededToStatus[transition]
|
||||
case "pending":
|
||||
return "queued"
|
||||
case "running":
|
||||
return inProgressToStatus[transition]
|
||||
case "canceling":
|
||||
return "canceling"
|
||||
case "canceled":
|
||||
return "canceled"
|
||||
case "failed":
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
|
||||
export const DisplayStatusLanguage = {
|
||||
loading: "Loading...",
|
||||
started: "Running",
|
||||
starting: "Starting",
|
||||
stopping: "Stopping",
|
||||
stopped: "Stopped",
|
||||
deleting: "Deleting",
|
||||
deleted: "Deleted",
|
||||
canceling: "Canceling action",
|
||||
canceled: "Canceled action",
|
||||
failed: "Failed",
|
||||
queued: "Queued",
|
||||
}
|
||||
|
||||
export const DisplayWorkspaceBuildStatusLanguage = {
|
||||
succeeded: "Succeeded",
|
||||
pending: "Pending",
|
||||
@@ -181,6 +104,7 @@ export const displayWorkspaceBuildDuration = (
|
||||
}
|
||||
|
||||
export const DisplayAgentStatusLanguage = {
|
||||
loading: "Loading...",
|
||||
connected: "⦿ Connected",
|
||||
connecting: "⦿ Connecting",
|
||||
disconnected: "◍ Disconnected",
|
||||
@@ -197,7 +121,7 @@ export const getDisplayAgentStatus = (
|
||||
case undefined:
|
||||
return {
|
||||
color: theme.palette.text.secondary,
|
||||
status: DisplayStatusLanguage.loading,
|
||||
status: DisplayAgentStatusLanguage.loading,
|
||||
}
|
||||
case "connected":
|
||||
return {
|
||||
@@ -245,10 +169,6 @@ export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {
|
||||
return transition === "start" && status === "succeeded"
|
||||
}
|
||||
|
||||
export const isWorkspaceDeleted = (workspace: TypesGen.Workspace): boolean => {
|
||||
return getWorkspaceStatus(workspace.latest_build) === succeededToStatus["delete"]
|
||||
}
|
||||
|
||||
export const defaultWorkspaceExtension = (
|
||||
__startDate?: dayjs.Dayjs,
|
||||
): TypesGen.PutExtendWorkspaceRequest => {
|
||||
@@ -270,11 +190,10 @@ type FaviconType =
|
||||
| "favicon-running"
|
||||
|
||||
export const getFaviconByStatus = (build: TypesGen.WorkspaceBuild): FaviconType => {
|
||||
const status = getWorkspaceStatus(build)
|
||||
switch (status) {
|
||||
switch (build.status) {
|
||||
case undefined:
|
||||
return "favicon"
|
||||
case "started":
|
||||
case "running":
|
||||
return "favicon-success"
|
||||
case "starting":
|
||||
return "favicon-running"
|
||||
@@ -290,10 +209,9 @@ export const getFaviconByStatus = (build: TypesGen.WorkspaceBuild): FaviconType
|
||||
return "favicon-warning"
|
||||
case "canceled":
|
||||
return "favicon"
|
||||
case "error":
|
||||
case "failed":
|
||||
return "favicon-error"
|
||||
case "queued":
|
||||
case "pending":
|
||||
return "favicon"
|
||||
}
|
||||
throw new Error("unknown status " + status)
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ export const workspaceItemMachine = createMachine(
|
||||
UPDATE_VERSION: {
|
||||
target: "gettingUpdatedTemplate",
|
||||
// We can improve the UI by optimistically updating the workspace status
|
||||
// to "Queued" so the UI can display the updated state right away and we
|
||||
// to "Pending" so the UI can display the updated state right away and we
|
||||
// don't need to display an extra spinner.
|
||||
actions: ["assignQueuedStatus", "displayUpdatingVersionMessage"],
|
||||
actions: ["assignPendingStatus", "displayUpdatingVersionMessage"],
|
||||
},
|
||||
UPDATE_DATA: {
|
||||
actions: "assignUpdatedData",
|
||||
@@ -153,12 +153,13 @@ export const workspaceItemMachine = createMachine(
|
||||
displayUpdatingVersionMessage: () => {
|
||||
displayMsg("Updating workspace...")
|
||||
},
|
||||
assignQueuedStatus: assign({
|
||||
assignPendingStatus: assign({
|
||||
data: (ctx) => {
|
||||
return {
|
||||
...ctx.data,
|
||||
latest_build: {
|
||||
...ctx.data.latest_build,
|
||||
status: "pending" as TypesGen.WorkspaceStatus,
|
||||
job: {
|
||||
...ctx.data.latest_build.job,
|
||||
status: "pending" as TypesGen.ProvisionerJobStatus,
|
||||
|
||||
Reference in New Issue
Block a user