Skip to content

Commit 5d8829e

Browse files
committed
Add status menu
1 parent 16bbf5a commit 5d8829e

File tree

3 files changed

+193
-2
lines changed

3 files changed

+193
-2
lines changed

site/src/pages/WorkspacesPage/WorkspaceSearch/PresetFilterMenu.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ const meta: Meta<typeof PresetFiltersMenu> = {
1010
export default meta;
1111
type Story = StoryObj<typeof PresetFiltersMenu>;
1212

13-
export const Closed: Story = {};
13+
export const Close: Story = {};
1414

15-
export const Opened: Story = {
15+
export const Open: Story = {
1616
play: async ({ canvasElement }) => {
1717
const canvas = within(canvasElement);
1818
const button = canvas.getByRole("button", { name: /filters/i });
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { type Theme, useTheme } from "@emotion/react";
2+
import CheckOutlined from "@mui/icons-material/CheckOutlined";
3+
import Button from "@mui/material/Button";
4+
import MenuItem from "@mui/material/MenuItem";
5+
import MenuList from "@mui/material/MenuList";
6+
import type { FC, ReactNode } from "react";
7+
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
8+
import {
9+
Popover,
10+
PopoverContent,
11+
PopoverTrigger,
12+
} from "components/Popover/Popover";
13+
import { Stack } from "components/Stack/Stack";
14+
15+
type StatusCircleProps = {
16+
color: keyof Theme["roles"];
17+
};
18+
19+
const StatusCircle: FC<StatusCircleProps> = ({ color }) => {
20+
const theme = useTheme();
21+
22+
return (
23+
<div
24+
css={{
25+
width: 8,
26+
height: 8,
27+
borderRadius: "50%",
28+
backgroundColor: theme.roles[color].fill.outline,
29+
}}
30+
/>
31+
);
32+
};
33+
34+
type Option = {
35+
label: string;
36+
value: string;
37+
addon: ReactNode;
38+
};
39+
40+
const options: Option[] = [
41+
{
42+
label: "Running",
43+
value: "running",
44+
addon: <StatusCircle color="success" />,
45+
},
46+
{
47+
label: "Stopped",
48+
value: "stopped",
49+
addon: <StatusCircle color="inactive" />,
50+
},
51+
{
52+
label: "Failed",
53+
value: "failed",
54+
addon: <StatusCircle color="error" />,
55+
},
56+
{
57+
label: "Pending",
58+
value: "pending",
59+
addon: <StatusCircle color="info" />,
60+
},
61+
];
62+
63+
type SelectLabelProps = {
64+
option: Option;
65+
selected: boolean;
66+
};
67+
68+
const SelectLabel: FC<SelectLabelProps> = ({ option, selected }) => {
69+
return (
70+
<Stack
71+
direction="row"
72+
alignItems="center"
73+
spacing={2}
74+
css={{ width: "100%", lineHeight: 1 }}
75+
>
76+
<span css={{ flexShrink: 0 }} role="presentation">
77+
{option.addon}
78+
</span>
79+
<span css={{ width: "100%" }}>{option.label}</span>
80+
<div css={{ width: 14, height: 14, flexShrink: 0 }} role="presentation">
81+
{selected && <CheckOutlined css={{ width: 14, height: 14 }} />}
82+
</div>
83+
</Stack>
84+
);
85+
};
86+
87+
type StatusMenuProps = {
88+
placeholder: string;
89+
selected: string | undefined;
90+
onSelect: (value: string) => void;
91+
};
92+
93+
export const StatusMenu: FC<StatusMenuProps> = (props) => {
94+
const { placeholder, selected, onSelect } = props;
95+
const selectedOption = options.find((option) => option.value === selected);
96+
97+
return (
98+
<Popover>
99+
{({ setIsOpen }) => {
100+
return (
101+
<>
102+
<PopoverTrigger>
103+
<Button
104+
aria-label="Select status"
105+
endIcon={<DropdownArrow />}
106+
startIcon={selectedOption?.addon}
107+
>
108+
{selectedOption ? selectedOption.label : placeholder}
109+
</Button>
110+
</PopoverTrigger>
111+
<PopoverContent>
112+
<MenuList dense>
113+
{options.map((option) => (
114+
<MenuItem
115+
selected={option.value === selected}
116+
key={option.value}
117+
onClick={() => {
118+
setIsOpen(false);
119+
onSelect(option.value);
120+
}}
121+
>
122+
<SelectLabel
123+
option={option}
124+
selected={option.value === selected}
125+
/>
126+
</MenuItem>
127+
))}
128+
</MenuList>
129+
</PopoverContent>
130+
</>
131+
);
132+
}}
133+
</Popover>
134+
);
135+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { userEvent, within } from "@storybook/test";
3+
import { useState } from "react";
4+
import { StatusMenu } from "./StatusMenu";
5+
6+
const meta: Meta<typeof StatusMenu> = {
7+
title: "pages/WorkspacesPage/StatusMenu",
8+
component: StatusMenu,
9+
args: {
10+
placeholder: "All statuses",
11+
},
12+
};
13+
14+
export default meta;
15+
type Story = StoryObj<typeof StatusMenu>;
16+
17+
export const Close: Story = {};
18+
19+
export const Open: Story = {
20+
play: async ({ canvasElement }) => {
21+
const canvas = within(canvasElement);
22+
const button = canvas.getByRole("button", { name: /Select status/i });
23+
await userEvent.click(button);
24+
},
25+
};
26+
27+
export const Selected: Story = {
28+
args: {
29+
selected: "running",
30+
},
31+
};
32+
33+
export const SelectedOpen: Story = {
34+
args: {
35+
selected: "running",
36+
},
37+
play: async ({ canvasElement }) => {
38+
const canvas = within(canvasElement);
39+
const button = canvas.getByRole("button", { name: /Select status/i });
40+
await userEvent.click(button);
41+
},
42+
};
43+
44+
export const SelectOption: Story = {
45+
render: function StatusMenuWithState(args) {
46+
const [selected, setSelected] = useState<string | undefined>(undefined);
47+
return <StatusMenu {...args} selected={selected} onSelect={setSelected} />;
48+
},
49+
play: async ({ canvasElement }) => {
50+
const canvas = within(canvasElement);
51+
const button = canvas.getByRole("button", { name: /Select status/i });
52+
await userEvent.click(button);
53+
const option = canvas.getByText("Failed");
54+
await userEvent.click(option);
55+
},
56+
};

0 commit comments

Comments
 (0)