Skip to content

Commit e769722

Browse files
committed
feat: Workspaces filtering
1 parent b85de3e commit e769722

File tree

5 files changed

+204
-94
lines changed

5 files changed

+204
-94
lines changed
Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,95 @@
1+
import Button from "@material-ui/core/Button"
2+
import Link from "@material-ui/core/Link"
3+
import Menu from "@material-ui/core/Menu"
4+
import MenuItem from "@material-ui/core/MenuItem"
5+
import { makeStyles } from "@material-ui/core/styles"
6+
import TextField from "@material-ui/core/TextField"
7+
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
18
import { useMachine } from "@xstate/react"
2-
import { FC } from "react"
9+
import { FormikContextType, FormikErrors, useFormik } from "formik"
10+
import { FC, useState } from "react"
11+
import { Link as RouterLink } from "react-router-dom"
12+
import { Margins } from "../../components/Margins/Margins"
13+
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
314
import { workspacesMachine } from "../../xServices/workspaces/workspacesXService"
4-
import { WorkspacesPageView } from "./WorkspacesPageView"
15+
import { Language, WorkspacesPageView } from "./WorkspacesPageView"
16+
17+
interface FilterFormValues {
18+
query: string
19+
}
20+
21+
export type FilterFormErrors = FormikErrors<FilterFormValues>
522

623
const WorkspacesPage: FC = () => {
7-
const [workspacesState] = useMachine(workspacesMachine)
24+
const styles = useStyles()
25+
const [workspacesState, send] = useMachine(workspacesMachine)
26+
27+
const form: FormikContextType<FilterFormValues> = useFormik<FilterFormValues>({
28+
initialValues: { query: workspacesState.context.filter || "" },
29+
onSubmit: (data) => {
30+
send({
31+
type: "SET_FILTER",
32+
query: data.query,
33+
})
34+
},
35+
})
36+
37+
const getFieldHelpers = getFormHelpers<FilterFormValues>(form, {})
38+
39+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
40+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
41+
setAnchorEl(event.currentTarget)
42+
}
43+
const handleClose = () => {
44+
setAnchorEl(null)
45+
}
46+
const setYourWorkspaces = () => {
47+
form.setFieldValue("query", "owner:me")
48+
void form.submitForm()
49+
handleClose()
50+
}
51+
const setAllWorkspaces = () => {
52+
form.setFieldValue("query", "")
53+
void form.submitForm()
54+
handleClose()
55+
}
856

957
return (
1058
<>
11-
<WorkspacesPageView
12-
loading={workspacesState.hasTag("loading")}
13-
workspaces={workspacesState.context.workspaces}
14-
error={workspacesState.context.getWorkspacesError}
15-
/>
59+
<Margins>
60+
<div className={styles.actions}>
61+
<Button aria-controls="simple-menu" aria-haspopup="true" onClick={handleClick}>
62+
Filter
63+
</Button>
64+
<Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
65+
<MenuItem onClick={setYourWorkspaces}>Your workspaces</MenuItem>
66+
<MenuItem onClick={setAllWorkspaces}>All workspaces</MenuItem>
67+
</Menu>
68+
<form onSubmit={form.handleSubmit}>
69+
<TextField {...getFieldHelpers("query")} onChange={onChangeTrimmed(form)} fullWidth variant="outlined" />
70+
</form>
71+
<Link underline="none" component={RouterLink} to="/workspaces/new">
72+
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
73+
</Link>
74+
</div>
75+
<WorkspacesPageView
76+
loading={workspacesState.hasTag("loading")}
77+
workspaces={workspacesState.context.workspaces}
78+
error={workspacesState.context.getWorkspacesError}
79+
/>
80+
</Margins>
1681
</>
1782
)
1883
}
1984

85+
const useStyles = makeStyles((theme) => ({
86+
actions: {
87+
marginTop: theme.spacing(3),
88+
marginBottom: theme.spacing(3),
89+
display: "flex",
90+
justifyContent: "space-between",
91+
height: theme.spacing(6),
92+
},
93+
}))
94+
2095
export default WorkspacesPage

site/src/pages/WorkspacesPage/WorkspacesPageView.tsx

Lines changed: 63 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { Link as RouterLink } from "react-router-dom"
1515
import * as TypesGen from "../../api/typesGenerated"
1616
import { AvatarData } from "../../components/AvatarData/AvatarData"
1717
import { EmptyState } from "../../components/EmptyState/EmptyState"
18-
import { Margins } from "../../components/Margins/Margins"
18+
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
1919
import { Stack } from "../../components/Stack/Stack"
2020
import { TableLoader } from "../../components/TableLoader/TableLoader"
2121
import { getDisplayStatus } from "../../util/workspace"
@@ -34,93 +34,78 @@ export interface WorkspacesPageViewProps {
3434
error?: unknown
3535
}
3636

37-
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = (props) => {
38-
const styles = useStyles()
37+
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, workspaces, error }) => {
38+
useStyles()
3939
const theme: Theme = useTheme()
40+
4041
return (
4142
<Stack spacing={4}>
42-
<Margins>
43-
<div className={styles.actions}>
44-
<Link underline="none" component={RouterLink} to="/workspaces/new">
45-
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
46-
</Link>
47-
</div>
48-
<Table>
49-
<TableHead>
43+
{error && <ErrorSummary error={error} />}
44+
<Table>
45+
<TableHead>
46+
<TableRow>
47+
<TableCell>Name</TableCell>
48+
<TableCell>Template</TableCell>
49+
<TableCell>Version</TableCell>
50+
<TableCell>Last Built</TableCell>
51+
<TableCell>Status</TableCell>
52+
</TableRow>
53+
</TableHead>
54+
<TableBody>
55+
{loading && <TableLoader />}
56+
{workspaces && workspaces.length === 0 && (
5057
<TableRow>
51-
<TableCell>Name</TableCell>
52-
<TableCell>Template</TableCell>
53-
<TableCell>Version</TableCell>
54-
<TableCell>Last Built</TableCell>
55-
<TableCell>Status</TableCell>
58+
<TableCell colSpan={999}>
59+
<EmptyState
60+
message={Language.emptyMessage}
61+
description={Language.emptyDescription}
62+
cta={
63+
<Link underline="none" component={RouterLink} to="/workspaces/new">
64+
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
65+
</Link>
66+
}
67+
/>
68+
</TableCell>
5669
</TableRow>
57-
</TableHead>
58-
<TableBody>
59-
{props.loading && <TableLoader />}
60-
{props.workspaces && props.workspaces.length === 0 && (
61-
<TableRow>
62-
<TableCell colSpan={999}>
63-
<EmptyState
64-
message={Language.emptyMessage}
65-
description={Language.emptyDescription}
66-
cta={
67-
<Link underline="none" component={RouterLink} to="/workspaces/new">
68-
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
69-
</Link>
70-
}
71-
/>
72-
</TableCell>
73-
</TableRow>
74-
)}
75-
{props.workspaces &&
76-
props.workspaces.map((workspace) => {
77-
const status = getDisplayStatus(theme, workspace.latest_build)
78-
return (
79-
<TableRow key={workspace.id}>
80-
<TableCell>
81-
<AvatarData
82-
title={workspace.name}
83-
subtitle={workspace.owner_name}
84-
link={`/workspaces/${workspace.id}`}
85-
/>
86-
</TableCell>
87-
<TableCell>{workspace.template_name}</TableCell>
88-
<TableCell>
89-
{workspace.outdated ? (
90-
<span style={{ color: theme.palette.error.main }}>outdated</span>
91-
) : (
92-
<span style={{ color: theme.palette.text.secondary }}>up to date</span>
93-
)}
94-
</TableCell>
95-
<TableCell>
96-
<span data-chromatic="ignore" style={{ color: theme.palette.text.secondary }}>
97-
{dayjs().to(dayjs(workspace.latest_build.created_at))}
98-
</span>
99-
</TableCell>
100-
<TableCell>
101-
<span style={{ color: status.color }}>{status.status}</span>
102-
</TableCell>
103-
</TableRow>
104-
)
105-
})}
106-
</TableBody>
107-
</Table>
108-
</Margins>
70+
)}
71+
{workspaces &&
72+
workspaces.map((workspace) => {
73+
const status = getDisplayStatus(theme, workspace.latest_build)
74+
return (
75+
<TableRow key={workspace.id}>
76+
<TableCell>
77+
<AvatarData
78+
title={workspace.name}
79+
subtitle={workspace.owner_name}
80+
link={`/workspaces/${workspace.id}`}
81+
/>
82+
</TableCell>
83+
<TableCell>{workspace.template_name}</TableCell>
84+
<TableCell>
85+
{workspace.outdated ? (
86+
<span style={{ color: theme.palette.error.main }}>outdated</span>
87+
) : (
88+
<span style={{ color: theme.palette.text.secondary }}>up to date</span>
89+
)}
90+
</TableCell>
91+
<TableCell>
92+
<span data-chromatic="ignore" style={{ color: theme.palette.text.secondary }}>
93+
{dayjs().to(dayjs(workspace.latest_build.created_at))}
94+
</span>
95+
</TableCell>
96+
<TableCell>
97+
<span style={{ color: status.color }}>{status.status}</span>
98+
</TableCell>
99+
</TableRow>
100+
)
101+
})}
102+
</TableBody>
103+
</Table>
109104
</Stack>
110105
)
111106
}
112107

113108
const useStyles = makeStyles((theme) => ({
114-
actions: {
115-
marginTop: theme.spacing(3),
116-
marginBottom: theme.spacing(3),
117-
display: "flex",
118-
height: theme.spacing(6),
119-
120-
"& > *": {
121-
marginLeft: "auto",
122-
},
123-
},
124109
welcome: {
125110
paddingTop: theme.spacing(12),
126111
paddingBottom: theme.spacing(12),

site/src/util/workspace.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as TypesGen from "../api/typesGenerated"
22
import * as Mocks from "../testHelpers/entities"
3-
import { isWorkspaceOn } from "./workspace"
3+
import { isWorkspaceOn, workspaceQueryToFilter } from "./workspace"
44

55
describe("util > workspace", () => {
66
describe("isWorkspaceOn", () => {
@@ -40,4 +40,15 @@ describe("util > workspace", () => {
4040
expect(isWorkspaceOn(workspace)).toBe(isOn)
4141
})
4242
})
43+
describe("workspaceQueryToFilter", () => {
44+
it.each<[string | undefined, TypesGen.WorkspaceFilter]>([
45+
[undefined, { Owner: "", OrganizationID: "" }],
46+
["", { Owner: "", OrganizationID: "" }],
47+
["asdkfvjn", { Owner: "", OrganizationID: "" }],
48+
["owner:me", { Owner: "me", OrganizationID: "" }],
49+
["owner:me owner:me2", { Owner: "me2", OrganizationID: "" }],
50+
])(`query=%p, filter=%p`, (query, filter) => {
51+
expect(workspaceQueryToFilter(query)).toBe(filter)
52+
})
53+
})
4354
})

site/src/util/workspace.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Theme } from "@material-ui/core/styles"
22
import dayjs from "dayjs"
33
import { WorkspaceBuildTransition } from "../api/types"
4-
import { Workspace, WorkspaceAgent, WorkspaceBuild } from "../api/typesGenerated"
4+
import { Workspace, WorkspaceAgent, WorkspaceBuild, WorkspaceFilter } from "../api/typesGenerated"
55

66
export type WorkspaceStatus =
77
| "queued"
@@ -191,3 +191,25 @@ export const isWorkspaceOn = (workspace: Workspace): boolean => {
191191
const status = workspace.latest_build.job.status
192192
return transition === "start" && status === "succeeded"
193193
}
194+
195+
export const workspaceQueryToFilter = (query?: string): WorkspaceFilter => {
196+
let filter: WorkspaceFilter = {
197+
Owner: "",
198+
OrganizationID: "",
199+
}
200+
201+
if (query) {
202+
const parts = query.split(" ")
203+
204+
parts.map((part) => {
205+
if (part.startsWith("owner:")) {
206+
filter = {
207+
Owner: part.slice("owner:".length),
208+
OrganizationID: "",
209+
}
210+
}
211+
})
212+
}
213+
214+
return filter
215+
}

0 commit comments

Comments
 (0)