Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit17ebec2

Browse files
f0sselgreyscaledKira-Pilot
authored andcommitted
feat: Workspaces filtering (#1972)
Co-authored-by: G r e y <grey@coder.com>Co-authored-by: Kira Pilot <kira@coder.com>
1 parentecf716f commit17ebec2

File tree

14 files changed

+377
-121
lines changed

14 files changed

+377
-121
lines changed

‎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+
ifarg.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 aboutcustomizing 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
@@ -137,17 +137,14 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
137137
// Empty strings mean no filter
138138
orgFilter:=r.URL.Query().Get("organization_id")
139139
ownerFilter:=r.URL.Query().Get("owner")
140+
nameFilter:=r.URL.Query().Get("name")
140141

141142
filter:= database.GetWorkspacesWithFilterParams{Deleted:false}
142143
iforgFilter!="" {
143144
orgID,err:=uuid.Parse(orgFilter)
144-
iferr!=nil {
145-
httpapi.Write(rw,http.StatusBadRequest, httpapi.Response{
146-
Message:fmt.Sprintf("organization_id must be a uuid: %s",err.Error()),
147-
})
148-
return
145+
iferr==nil {
146+
filter.OrganizationID=orgID
149147
}
150-
filter.OrganizationID=orgID
151148
}
152149
ifownerFilter=="me" {
153150
filter.OwnerID=apiKey.UserID
@@ -160,15 +157,15 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
160157
Username:ownerFilter,
161158
Email:ownerFilter,
162159
})
163-
iferr!=nil {
164-
httpapi.Write(rw,http.StatusBadRequest, httpapi.Response{
165-
Message:"owner must be a uuid or username",
166-
})
167-
return
160+
iferr==nil {
161+
filter.OwnerID=user.ID
168162
}
169-
userID=user.ID
163+
}else {
164+
filter.OwnerID=userID
170165
}
171-
filter.OwnerID=userID
166+
}
167+
ifnameFilter!="" {
168+
filter.Name=nameFilter
172169
}
173170

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

‎coderd/workspaces_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,37 @@ func TestWorkspacesByOwner(t *testing.T) {
268268
require.NoError(t,err)
269269
require.Len(t,workspaces,1)
270270
})
271+
272+
t.Run("ListName",func(t*testing.T) {
273+
t.Parallel()
274+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerD:true})
275+
user:=coderdtest.CreateFirstUser(t,client)
276+
277+
version:=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,nil)
278+
coderdtest.AwaitTemplateVersionJob(t,client,version.ID)
279+
template:=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
280+
w:=coderdtest.CreateWorkspace(t,client,user.OrganizationID,template.ID)
281+
282+
// Create noise workspace that should be filtered out
283+
_=coderdtest.CreateWorkspace(t,client,user.OrganizationID,template.ID)
284+
285+
// Use name filter
286+
workspaces,err:=client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
287+
Name:w.Name,
288+
})
289+
require.NoError(t,err)
290+
require.Len(t,workspaces,1)
291+
292+
// Create same name workspace that should be included
293+
other:=coderdtest.CreateAnotherUser(t,client,user.OrganizationID)
294+
_=coderdtest.CreateWorkspace(t,other,user.OrganizationID,template.ID,func(cwr*codersdk.CreateWorkspaceRequest) {cwr.Name=w.Name })
295+
296+
workspaces,err=client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
297+
Name:w.Name,
298+
})
299+
require.NoError(t,err)
300+
require.Len(t,workspaces,2)
301+
})
271302
}
272303

273304
funcTestWorkspaceByOwnerAndName(t*testing.T) {

‎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
typeWorkspaceFilterstruct {
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-
Ownerstring
203+
Ownerstring`json:"owner,omitempty"`
204+
Namestring`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
iff.Owner!="" {
215216
q.Set("owner",f.Owner)
216217
}
218+
iff.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
constbasePath="/api/v2/workspaces"
118118
constsearchParams=newURLSearchParams()
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
constsearchString=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
exportinterfaceWorkspaceFilter{
446-
readonlyOrganizationID:string
447-
readonlyOwner:string
446+
readonlyorganization_id?:string
447+
readonlyowner?:string
448+
readonlyname?: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+
importButtonfrom"@material-ui/core/Button"
2+
importFadefrom"@material-ui/core/Fade"
3+
importInputAdornmentfrom"@material-ui/core/InputAdornment"
4+
importLinkfrom"@material-ui/core/Link"
5+
importMenufrom"@material-ui/core/Menu"
6+
importMenuItemfrom"@material-ui/core/MenuItem"
7+
import{makeStyles}from"@material-ui/core/styles"
8+
importTextFieldfrom"@material-ui/core/TextField"
9+
importAddCircleOutlinefrom"@material-ui/icons/AddCircleOutline"
10+
importSearchIconfrom"@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{LinkasRouterLink}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+
interfaceFilterFormValues{
23+
query:string
24+
}
25+
26+
constLanguage={
27+
filterName:"Filters",
28+
createWorkspaceButton:"Create workspace",
29+
yourWorkspacesButton:"Your workspaces",
30+
allWorkspacesButton:"All workspaces",
31+
}
32+
33+
exporttypeFilterFormErrors=FormikErrors<FilterFormValues>
34+
635
constWorkspacesPage:FC=()=>{
7-
const[workspacesState]=useMachine(workspacesMachine)
36+
conststyles=useStyles()
37+
const[workspacesState,send]=useMachine(workspacesMachine)
38+
39+
constform=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+
constgetFieldHelpers=getFormHelpers<FilterFormValues>(form)
52+
53+
const[anchorEl,setAnchorEl]=useState<null|HTMLElement>(null)
54+
55+
consthandleClick=(event:React.MouseEvent<HTMLButtonElement>)=>{
56+
setAnchorEl(event.currentTarget)
57+
}
58+
59+
consthandleClose=()=>{
60+
setAnchorEl(null)
61+
}
62+
63+
constsetYourWorkspaces=()=>{
64+
voidform.setFieldValue("query","owner:me")
65+
voidform.submitForm()
66+
handleClose()
67+
}
68+
69+
constsetAllWorkspaces=()=>{
70+
voidform.setFieldValue("query","")
71+
voidform.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+
<Stackdirection="row"className={styles.workspacesHeaderContainer}>
78+
<Stackdirection="column"className={styles.filterColumn}>
79+
<Stackdirection="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+
<formonSubmit={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+
<InputAdornmentposition="start">
99+
<SearchIconfontSize="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+
<MenuItemonClick={setYourWorkspaces}>{Language.yourWorkspacesButton}</MenuItem>
123+
<MenuItemonClick={setAllWorkspaces}>{Language.allWorkspacesButton}</MenuItem>
124+
</Menu>
125+
</Stack>
126+
</Stack>
127+
128+
<Linkunderline="none"component={RouterLink}to="/workspaces/new">
129+
<ButtonstartIcon={<AddCircleOutline/>}style={{height:"44px"}}>
130+
{Language.createWorkspaceButton}
131+
</Button>
132+
</Link>
133+
</Stack>
134+
<WorkspacesPageViewloading={workspacesState.hasTag("loading")}workspaces={workspacesState.context.workspaces}/>
135+
</Margins>
17136
)
18137
}
19138

139+
constuseStyles=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
exportdefaultWorkspacesPage

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp