Skip to content

Commit 12b6c73

Browse files
committed
Merge branch 'main' into fixwsroutes
2 parents f82a755 + 60102cb commit 12b6c73

File tree

15 files changed

+348
-127
lines changed

15 files changed

+348
-127
lines changed

.github/codecov.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ coverage:
1919
ignore:
2020
# This is generated code.
2121
- coderd/database/models.go
22-
- coderd/database/query.sql.go
22+
- coderd/database/queries.sql.go
2323
- coderd/database/databasefake
2424
# These are generated or don't require tests.
2525
- cmd

cli/delete.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@ func delete() *cobra.Command {
3030
if err != nil {
3131
return err
3232
}
33-
organization, err := currentOrganization(cmd, client)
34-
if err != nil {
35-
return err
36-
}
37-
workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0])
33+
workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, args[0])
3834
if err != nil {
3935
return err
4036
}

coderd/database/databasefake/databasefake.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ func (q *fakeQuerier) GetWorkspacesWithFilter(_ context.Context, arg database.Ge
328328
if !arg.Deleted && workspace.Deleted {
329329
continue
330330
}
331+
if arg.Name != "" && workspace.Name != arg.Name {
332+
continue
333+
}
331334
workspaces = append(workspaces, workspace)
332335
}
333336

coderd/database/queries.sql.go

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaces.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ WHERE
2828
owner_id = @owner_id
2929
ELSE true
3030
END
31+
-- Filter by name
32+
AND CASE
33+
WHEN @name :: text != '' THEN
34+
LOWER(name) = LOWER(@name)
35+
ELSE true
36+
END
3137
;
3238

3339
-- name: GetWorkspacesByOrganizationIDs :many

coderd/workspaces.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,14 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
108108
// Empty strings mean no filter
109109
orgFilter := r.URL.Query().Get("organization_id")
110110
ownerFilter := r.URL.Query().Get("owner")
111+
nameFilter := r.URL.Query().Get("name")
111112

112113
filter := database.GetWorkspacesWithFilterParams{Deleted: false}
113114
if orgFilter != "" {
114115
orgID, err := uuid.Parse(orgFilter)
115-
if err != nil {
116-
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
117-
Message: fmt.Sprintf("organization_id must be a uuid: %s", err.Error()),
118-
})
119-
return
116+
if err == nil {
117+
filter.OrganizationID = orgID
120118
}
121-
filter.OrganizationID = orgID
122119
}
123120
if ownerFilter == "me" {
124121
filter.OwnerID = apiKey.UserID
@@ -131,15 +128,15 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
131128
Username: ownerFilter,
132129
Email: ownerFilter,
133130
})
134-
if err != nil {
135-
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
136-
Message: "owner must be a uuid or username",
137-
})
138-
return
131+
if err == nil {
132+
filter.OwnerID = user.ID
139133
}
140-
userID = user.ID
134+
} else {
135+
filter.OwnerID = userID
141136
}
142-
filter.OwnerID = userID
137+
}
138+
if nameFilter != "" {
139+
filter.Name = nameFilter
143140
}
144141

145142
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), filter)

codersdk/workspaces.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,10 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx
198198
}
199199

200200
type WorkspaceFilter struct {
201-
OrganizationID uuid.UUID
201+
OrganizationID uuid.UUID `json:"organization_id,omitempty"`
202202
// Owner can be a user_id (uuid), "me", or a username
203-
Owner string
203+
Owner string `json:"owner,omitempty"`
204+
Name string `json:"name,omitempty"`
204205
}
205206

206207
// asRequestOption returns a function that can be used in (*Client).Request.
@@ -214,6 +215,9 @@ func (f WorkspaceFilter) asRequestOption() requestOption {
214215
if f.Owner != "" {
215216
q.Set("owner", f.Owner)
216217
}
218+
if f.Name != "" {
219+
q.Set("name", f.Name)
220+
}
217221
r.URL.RawQuery = q.Encode()
218222
}
219223
}

site/src/api/api.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ describe("api.ts", () => {
118118
it.each<[TypesGen.WorkspaceFilter | undefined, string]>([
119119
[undefined, "/api/v2/workspaces"],
120120

121-
[{ OrganizationID: "1", Owner: "" }, "/api/v2/workspaces?organization_id=1"],
122-
[{ OrganizationID: "", Owner: "1" }, "/api/v2/workspaces?owner=1"],
121+
[{ organization_id: "1", owner: "" }, "/api/v2/workspaces?organization_id=1"],
122+
[{ organization_id: "", owner: "1" }, "/api/v2/workspaces?owner=1"],
123123

124-
[{ OrganizationID: "1", Owner: "me" }, "/api/v2/workspaces?organization_id=1&owner=me"],
124+
[{ organization_id: "1", owner: "me" }, "/api/v2/workspaces?organization_id=1&owner=me"],
125125
])(`getWorkspacesURL(%p) returns %p`, (filter, expected) => {
126126
expect(getWorkspacesURL(filter)).toBe(expected)
127127
})

site/src/api/api.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,14 @@ export const getWorkspacesURL = (filter?: TypesGen.WorkspaceFilter): string => {
117117
const basePath = "/api/v2/workspaces"
118118
const searchParams = new URLSearchParams()
119119

120-
if (filter?.OrganizationID) {
121-
searchParams.append("organization_id", filter.OrganizationID)
120+
if (filter?.organization_id) {
121+
searchParams.append("organization_id", filter.organization_id)
122122
}
123-
if (filter?.Owner) {
124-
searchParams.append("owner", filter.Owner)
123+
if (filter?.owner) {
124+
searchParams.append("owner", filter.owner)
125+
}
126+
if (filter?.name) {
127+
searchParams.append("name", filter.name)
125128
}
126129

127130
const searchString = searchParams.toString()

site/src/api/typesGenerated.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,9 @@ export interface WorkspaceBuildsRequest extends Pagination {
443443

444444
// From codersdk/workspaces.go:200:6
445445
export interface WorkspaceFilter {
446-
readonly OrganizationID: string
447-
readonly Owner: string
446+
readonly organization_id?: string
447+
readonly owner?: string
448+
readonly name?: string
448449
}
449450

450451
// From codersdk/workspaceresources.go:21:6
Lines changed: 158 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,169 @@
1+
import Button from "@material-ui/core/Button"
2+
import Fade from "@material-ui/core/Fade"
3+
import InputAdornment from "@material-ui/core/InputAdornment"
4+
import Link from "@material-ui/core/Link"
5+
import Menu from "@material-ui/core/Menu"
6+
import MenuItem from "@material-ui/core/MenuItem"
7+
import { makeStyles } from "@material-ui/core/styles"
8+
import TextField from "@material-ui/core/TextField"
9+
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
10+
import SearchIcon from "@material-ui/icons/Search"
111
import { useMachine } from "@xstate/react"
2-
import { FC } from "react"
12+
import { FormikErrors, useFormik } from "formik"
13+
import { FC, useState } from "react"
14+
import { Link as RouterLink } from "react-router-dom"
15+
import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/DropdownArrows"
16+
import { Margins } from "../../components/Margins/Margins"
17+
import { Stack } from "../../components/Stack/Stack"
18+
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
319
import { workspacesMachine } from "../../xServices/workspaces/workspacesXService"
420
import { WorkspacesPageView } from "./WorkspacesPageView"
521

22+
interface FilterFormValues {
23+
query: string
24+
}
25+
26+
const Language = {
27+
filterName: "Filters",
28+
createWorkspaceButton: "Create workspace",
29+
yourWorkspacesButton: "Your workspaces",
30+
allWorkspacesButton: "All workspaces",
31+
}
32+
33+
export type FilterFormErrors = FormikErrors<FilterFormValues>
34+
635
const WorkspacesPage: FC = () => {
7-
const [workspacesState] = useMachine(workspacesMachine)
36+
const styles = useStyles()
37+
const [workspacesState, send] = useMachine(workspacesMachine)
38+
39+
const form = useFormik<FilterFormValues>({
40+
initialValues: {
41+
query: workspacesState.context.filter || "",
42+
},
43+
onSubmit: (values) => {
44+
send({
45+
type: "SET_FILTER",
46+
query: values.query,
47+
})
48+
},
49+
})
50+
51+
const getFieldHelpers = getFormHelpers<FilterFormValues>(form)
52+
53+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
54+
55+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
56+
setAnchorEl(event.currentTarget)
57+
}
58+
59+
const handleClose = () => {
60+
setAnchorEl(null)
61+
}
62+
63+
const setYourWorkspaces = () => {
64+
void form.setFieldValue("query", "owner:me")
65+
void form.submitForm()
66+
handleClose()
67+
}
68+
69+
const setAllWorkspaces = () => {
70+
void form.setFieldValue("query", "")
71+
void form.submitForm()
72+
handleClose()
73+
}
874

975
return (
10-
<>
11-
<WorkspacesPageView
12-
loading={workspacesState.hasTag("loading")}
13-
workspaces={workspacesState.context.workspaces}
14-
error={workspacesState.context.getWorkspacesError}
15-
/>
16-
</>
76+
<Margins>
77+
<Stack direction="row" className={styles.workspacesHeaderContainer}>
78+
<Stack direction="column" className={styles.filterColumn}>
79+
<Stack direction="row" spacing={0} className={styles.filterContainer}>
80+
<Button
81+
aria-controls="filter-menu"
82+
aria-haspopup="true"
83+
onClick={handleClick}
84+
className={styles.buttonRoot}
85+
>
86+
{Language.filterName} {anchorEl ? <CloseDropdown /> : <OpenDropdown />}
87+
</Button>
88+
89+
<form onSubmit={form.handleSubmit} className={styles.filterForm}>
90+
<TextField
91+
{...getFieldHelpers("query")}
92+
className={styles.textFieldRoot}
93+
onChange={onChangeTrimmed(form)}
94+
fullWidth
95+
variant="outlined"
96+
InputProps={{
97+
startAdornment: (
98+
<InputAdornment position="start">
99+
<SearchIcon fontSize="small" />
100+
</InputAdornment>
101+
),
102+
}}
103+
/>
104+
</form>
105+
106+
<Menu
107+
id="filter-menu"
108+
anchorEl={anchorEl}
109+
keepMounted
110+
open={Boolean(anchorEl)}
111+
onClose={handleClose}
112+
TransitionComponent={Fade}
113+
anchorOrigin={{
114+
vertical: "bottom",
115+
horizontal: "left",
116+
}}
117+
transformOrigin={{
118+
vertical: "top",
119+
horizontal: "left",
120+
}}
121+
>
122+
<MenuItem onClick={setYourWorkspaces}>{Language.yourWorkspacesButton}</MenuItem>
123+
<MenuItem onClick={setAllWorkspaces}>{Language.allWorkspacesButton}</MenuItem>
124+
</Menu>
125+
</Stack>
126+
</Stack>
127+
128+
<Link underline="none" component={RouterLink} to="/workspaces/new">
129+
<Button startIcon={<AddCircleOutline />} style={{ height: "44px" }}>
130+
{Language.createWorkspaceButton}
131+
</Button>
132+
</Link>
133+
</Stack>
134+
<WorkspacesPageView loading={workspacesState.hasTag("loading")} workspaces={workspacesState.context.workspaces} />
135+
</Margins>
17136
)
18137
}
19138

139+
const useStyles = makeStyles((theme) => ({
140+
workspacesHeaderContainer: {
141+
marginTop: theme.spacing(3),
142+
marginBottom: theme.spacing(3),
143+
justifyContent: "space-between",
144+
},
145+
filterColumn: {
146+
width: "60%",
147+
cursor: "text",
148+
},
149+
filterContainer: {
150+
border: `1px solid ${theme.palette.divider}`,
151+
borderRadius: "6px",
152+
},
153+
filterForm: {
154+
width: "100%",
155+
},
156+
buttonRoot: {
157+
border: "none",
158+
borderRight: `1px solid ${theme.palette.divider}`,
159+
borderRadius: "6px 0px 0px 6px",
160+
},
161+
textFieldRoot: {
162+
margin: "0px",
163+
"& fieldset": {
164+
border: "none",
165+
},
166+
},
167+
}))
168+
20169
export default WorkspacesPage

0 commit comments

Comments
 (0)