Skip to content

Commit eebf0dd

Browse files
authored
feat: consolidate workspace buttons/kira pilot (#2996)
* added workspace cta dropdown resolves #2748 * added tests * fixed failing tests * clean up snapshots
1 parent aea3b3b commit eebf0dd

File tree

6 files changed

+507
-91
lines changed

6 files changed

+507
-91
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import Button from "@material-ui/core/Button"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import CloudQueueIcon from "@material-ui/icons/CloudQueue"
4+
import CropSquareIcon from "@material-ui/icons/CropSquare"
5+
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
6+
import HighlightOffIcon from "@material-ui/icons/HighlightOff"
7+
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
8+
import { FC } from "react"
9+
import { Workspace } from "../../api/typesGenerated"
10+
import { WorkspaceStatus } from "../../util/workspace"
11+
import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton"
12+
13+
export const Language = {
14+
start: "Start",
15+
stop: "Stop",
16+
delete: "Delete",
17+
cancel: "Cancel",
18+
update: "Update",
19+
}
20+
21+
interface WorkspaceAction {
22+
handleAction: () => void
23+
}
24+
25+
export const StartButton: FC<WorkspaceAction> = ({ handleAction }) => {
26+
const styles = useStyles()
27+
28+
return (
29+
<WorkspaceActionButton
30+
className={styles.actionButton}
31+
icon={<PlayCircleOutlineIcon />}
32+
onClick={handleAction}
33+
label={Language.start}
34+
/>
35+
)
36+
}
37+
38+
export const StopButton: FC<WorkspaceAction> = ({ handleAction }) => {
39+
const styles = useStyles()
40+
41+
return (
42+
<WorkspaceActionButton
43+
className={styles.actionButton}
44+
icon={<CropSquareIcon />}
45+
onClick={handleAction}
46+
label={Language.stop}
47+
/>
48+
)
49+
}
50+
51+
export const DeleteButton: FC<WorkspaceAction> = ({ handleAction }) => {
52+
const styles = useStyles()
53+
54+
return (
55+
<WorkspaceActionButton
56+
className={styles.actionButton}
57+
icon={<DeleteOutlineIcon />}
58+
onClick={handleAction}
59+
label={Language.delete}
60+
/>
61+
)
62+
}
63+
64+
type UpdateAction = WorkspaceAction & {
65+
workspace: Workspace
66+
workspaceStatus: WorkspaceStatus
67+
}
68+
69+
export const UpdateButton: FC<UpdateAction> = ({ handleAction, workspace, workspaceStatus }) => {
70+
const styles = useStyles()
71+
72+
/**
73+
* Jobs submitted while another job is in progress will be discarded,
74+
* so check whether workspace job status has reached completion (whether successful or not).
75+
*/
76+
const canAcceptJobs = (workspaceStatus: WorkspaceStatus) =>
77+
["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus)
78+
79+
return (
80+
<>
81+
{workspace.outdated && canAcceptJobs(workspaceStatus) && (
82+
<Button
83+
className={styles.actionButton}
84+
startIcon={<CloudQueueIcon />}
85+
onClick={handleAction}
86+
>
87+
{Language.update}
88+
</Button>
89+
)}
90+
</>
91+
)
92+
}
93+
94+
export const CancelButton: FC<WorkspaceAction> = ({ handleAction }) => {
95+
const styles = useStyles()
96+
97+
return (
98+
<WorkspaceActionButton
99+
className={styles.actionButton}
100+
icon={<HighlightOffIcon />}
101+
onClick={handleAction}
102+
label={Language.cancel}
103+
/>
104+
)
105+
}
106+
107+
const useStyles = makeStyles((theme) => ({
108+
actionButton: {
109+
// Set fixed width for the action buttons so they will not change the size
110+
// during the transitions
111+
width: theme.spacing(16),
112+
border: "none",
113+
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
114+
},
115+
}))
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { action } from "@storybook/addon-actions"
2+
import { Story } from "@storybook/react"
3+
import * as Mocks from "../../testHelpers/entities"
4+
import { WorkspaceActions, WorkspaceActionsProps } from "./WorkspaceActions"
5+
6+
export default {
7+
title: "components/WorkspaceActions",
8+
component: WorkspaceActions,
9+
}
10+
11+
const Template: Story<WorkspaceActionsProps> = (args) => <WorkspaceActions {...args} />
12+
13+
const defaultArgs = {
14+
handleStart: action("start"),
15+
handleStop: action("stop"),
16+
handleDelete: action("delete"),
17+
handleUpdate: action("update"),
18+
handleCancel: action("cancel"),
19+
}
20+
21+
export const Starting = Template.bind({})
22+
Starting.args = {
23+
...defaultArgs,
24+
workspace: Mocks.MockStartingWorkspace,
25+
}
26+
27+
export const Started = Template.bind({})
28+
Started.args = {
29+
...defaultArgs,
30+
workspace: Mocks.MockWorkspace,
31+
}
32+
33+
export const Stopping = Template.bind({})
34+
Stopping.args = {
35+
...defaultArgs,
36+
workspace: Mocks.MockStoppingWorkspace,
37+
}
38+
39+
export const Stopped = Template.bind({})
40+
Stopped.args = {
41+
...defaultArgs,
42+
workspace: Mocks.MockStoppedWorkspace,
43+
}
44+
45+
export const Canceling = Template.bind({})
46+
Canceling.args = {
47+
...defaultArgs,
48+
workspace: Mocks.MockCancelingWorkspace,
49+
}
50+
51+
export const Canceled = Template.bind({})
52+
Canceled.args = {
53+
...defaultArgs,
54+
workspace: Mocks.MockCanceledWorkspace,
55+
}
56+
57+
export const Deleting = Template.bind({})
58+
Deleting.args = {
59+
...defaultArgs,
60+
workspace: Mocks.MockDeletingWorkspace,
61+
}
62+
63+
export const Deleted = Template.bind({})
64+
Deleted.args = {
65+
...defaultArgs,
66+
workspace: Mocks.MockDeletedWorkspace,
67+
}
68+
69+
export const Outdated = Template.bind({})
70+
Outdated.args = {
71+
...defaultArgs,
72+
workspace: Mocks.MockOutdatedWorkspace,
73+
}
74+
75+
export const Errored = Template.bind({})
76+
Errored.args = {
77+
...defaultArgs,
78+
workspace: Mocks.MockFailedWorkspace,
79+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { screen } from "@testing-library/react"
2+
import * as Mocks from "../../testHelpers/entities"
3+
import { render } from "../../testHelpers/renderHelpers"
4+
import { Language } from "./ActionCtas"
5+
import { WorkspaceStateEnum } from "./constants"
6+
import { WorkspaceActions, WorkspaceActionsProps } from "./WorkspaceActions"
7+
8+
const renderAndClick = async (props: Partial<WorkspaceActionsProps> = {}) => {
9+
render(
10+
<WorkspaceActions
11+
workspace={props.workspace ?? Mocks.MockWorkspace}
12+
handleStart={jest.fn()}
13+
handleStop={jest.fn()}
14+
handleDelete={jest.fn()}
15+
handleUpdate={jest.fn()}
16+
handleCancel={jest.fn()}
17+
/>,
18+
)
19+
const trigger = await screen.findByTestId("workspace-actions-button")
20+
trigger.click()
21+
}
22+
23+
describe("WorkspaceActions", () => {
24+
describe("when the workspace is starting", () => {
25+
it("primary is cancel; no secondary", async () => {
26+
await renderAndClick({ workspace: Mocks.MockStartingWorkspace })
27+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.cancel)
28+
expect(screen.queryByTestId("secondary-ctas")).toBeNull()
29+
})
30+
})
31+
describe("when the workspace is started", () => {
32+
it("primary is stop; secondary is delete", async () => {
33+
await renderAndClick({ workspace: Mocks.MockWorkspace })
34+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.stop)
35+
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
36+
})
37+
})
38+
describe("when the workspace is stopping", () => {
39+
it("primary is cancel; no secondary", async () => {
40+
await renderAndClick({ workspace: Mocks.MockStoppingWorkspace })
41+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.cancel)
42+
expect(screen.queryByTestId("secondary-ctas")).toBeNull()
43+
})
44+
})
45+
describe("when the workspace is canceling", () => {
46+
it("primary is canceling; no secondary", async () => {
47+
await renderAndClick({ workspace: Mocks.MockCancelingWorkspace })
48+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(WorkspaceStateEnum.canceling)
49+
expect(screen.queryByTestId("secondary-ctas")).toBeNull()
50+
})
51+
})
52+
describe("when the workspace is canceled", () => {
53+
it("primary is start; secondary are stop, delete", async () => {
54+
await renderAndClick({ workspace: Mocks.MockCanceledWorkspace })
55+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.start)
56+
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.stop)
57+
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
58+
})
59+
})
60+
describe("when the workspace is errored", () => {
61+
it("primary is start; secondary is delete", async () => {
62+
await renderAndClick({ workspace: Mocks.MockFailedWorkspace })
63+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.start)
64+
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
65+
})
66+
})
67+
describe("when the workspace is deleting", () => {
68+
it("primary is cancel; no secondary", async () => {
69+
await renderAndClick({ workspace: Mocks.MockDeletingWorkspace })
70+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.cancel)
71+
expect(screen.queryByTestId("secondary-ctas")).toBeNull()
72+
})
73+
})
74+
describe("when the workspace is deleted", () => {
75+
it("primary is deleted; no secondary", async () => {
76+
await renderAndClick({ workspace: Mocks.MockDeletedWorkspace })
77+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(WorkspaceStateEnum.deleted)
78+
expect(screen.queryByTestId("secondary-ctas")).toBeNull()
79+
})
80+
})
81+
describe("when the workspace is outdated", () => {
82+
it("primary is start; secondary are delete, update", async () => {
83+
await renderAndClick({ workspace: Mocks.MockOutdatedWorkspace })
84+
expect(screen.getByTestId("primary-cta")).toHaveTextContent(Language.start)
85+
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.delete)
86+
expect(screen.getByTestId("secondary-ctas")).toHaveTextContent(Language.update)
87+
})
88+
})
89+
})

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy