Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e3e82d7

Browse files
committed
Add filter search on Users page
1 parent 3312c81 commit e3e82d7

File tree

14 files changed

+108
-53
lines changed

14 files changed

+108
-53
lines changed

site/src/api/api.test.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import axios from "axios"
2-
import { getApiKey, getWorkspacesURL, login, logout } from "./api"
2+
import { getApiKey, getURLWithSearchParams, login, logout } from "./api"
33
import * as TypesGen from "./typesGenerated"
44

55
describe("api.ts", () => {
@@ -114,16 +114,19 @@ describe("api.ts", () => {
114114
})
115115
})
116116

117-
describe("getWorkspacesURL", () => {
118-
it.each<[TypesGen.WorkspaceFilter | undefined, string]>([
119-
[undefined, "/api/v2/workspaces"],
117+
describe("getURLWithSearchParams", () => {
118+
it.each<[string, TypesGen.WorkspaceFilter | TypesGen.UsersRequest | undefined, string]>([
119+
["/api/v2/workspaces", undefined, "/api/v2/workspaces"],
120120

121-
[{ q: "" }, "/api/v2/workspaces"],
122-
[{ q: "owner:1" }, "/api/v2/workspaces?q=owner%3A1"],
121+
["/api/v2/workspaces", { q: "" }, "/api/v2/workspaces"],
122+
["/api/v2/workspaces", { q: "owner:1" }, "/api/v2/workspaces?q=owner%3A1"],
123123

124-
[{ q: "owner:me" }, "/api/v2/workspaces?q=owner%3Ame"],
125-
])(`getWorkspacesURL(%p) returns %p`, (filter, expected) => {
126-
expect(getWorkspacesURL(filter)).toBe(expected)
124+
["/api/v2/workspaces", { q: "owner:me" }, "/api/v2/workspaces?q=owner%3Ame"],
125+
126+
["/api/v2/users", { q: "status:active" }, "/api/v2/users?q=status%3Aactive"],
127+
["/api/v2/users", { q: "" }, "/api/v2/users"],
128+
])(`getURLWithSearchParams(%p) returns %p`, (basePath, filter, expected) => {
129+
expect(getURLWithSearchParams(basePath, filter)).toBe(expected)
127130
})
128131
})
129132
})

site/src/api/api.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
6262
return response.data
6363
}
6464

65-
export const getUsers = async (): Promise<TypesGen.User[]> => {
66-
const response = await axios.get<TypesGen.User[]>("/api/v2/users?q=status:active,suspended")
65+
export const getUsers = async (filter?: TypesGen.UsersRequest): Promise<TypesGen.User[]> => {
66+
const url = getURLWithSearchParams("/api/v2/users", filter)
67+
const response = await axios.get<TypesGen.User[]>(url)
6768
return response.data
6869
}
6970

@@ -115,8 +116,10 @@ export const getWorkspace = async (
115116
return response.data
116117
}
117118

118-
export const getWorkspacesURL = (filter?: TypesGen.WorkspaceFilter): string => {
119-
const basePath = "/api/v2/workspaces"
119+
export const getURLWithSearchParams = (
120+
basePath: string,
121+
filter?: TypesGen.WorkspaceFilter | TypesGen.UsersRequest,
122+
): string => {
120123
const searchParams = new URLSearchParams()
121124

122125
if (filter?.q && filter.q !== "") {
@@ -129,7 +132,7 @@ export const getWorkspacesURL = (filter?: TypesGen.WorkspaceFilter): string => {
129132
}
130133

131134
export const getWorkspaces = async (filter?: TypesGen.WorkspaceFilter): Promise<TypesGen.Workspace[]> => {
132-
const url = getWorkspacesURL(filter)
135+
const url = getURLWithSearchParams("/api/v2/workspaces", filter)
133136
const response = await axios.get<TypesGen.Workspace[]>(url)
134137
return response.data
135138
}

site/src/components/SearchBarWithFilter/SearchBarWithFilter.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ComponentMeta, Story } from "@storybook/react"
2-
import { workspaceFilterQuery } from "../../util/workspace"
2+
import { workspaceFilterQuery } from "../../util/filters"
33
import { SearchBarWithFilter, SearchBarWithFilterProps } from "./SearchBarWithFilter"
44

55
export default {

site/src/pages/UsersPage/UsersPage.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { useActor, useSelector } from "@xstate/react"
22
import React, { useContext, useEffect } from "react"
33
import { Helmet } from "react-helmet"
44
import { useNavigate } from "react-router"
5+
import { useSearchParams } from "react-router-dom"
56
import { ConfirmDialog } from "../../components/ConfirmDialog/ConfirmDialog"
67
import { ResetPasswordDialog } from "../../components/ResetPasswordDialog/ResetPasswordDialog"
8+
import { userFilterQuery } from "../../util/filters"
79
import { pageTitle } from "../../util/page"
810
import { selectPermissions } from "../../xServices/auth/authSelectors"
911
import { XServiceContext } from "../../xServices/StateContext"
@@ -25,6 +27,7 @@ export const UsersPage: React.FC = () => {
2527
const { users, getUsersError, userIdToSuspend, userIdToActivate, userIdToResetPassword, newUserPassword } =
2628
usersState.context
2729
const navigate = useNavigate()
30+
const [searchParams, setSearchParams] = useSearchParams()
2831
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
2932
const userToBeActivated = users?.find((u) => u.id === userIdToActivate)
3033
const userToResetPassword = users?.find((u) => u.id === userIdToResetPassword)
@@ -40,8 +43,13 @@ export const UsersPage: React.FC = () => {
4043

4144
// Fetch users on component mount
4245
useEffect(() => {
43-
usersSend("GET_USERS")
44-
}, [usersSend])
46+
const filter = searchParams.get("filter")
47+
const query = filter !== null ? filter : userFilterQuery.active
48+
usersSend({
49+
type: "GET_USERS",
50+
query,
51+
})
52+
}, [searchParams, usersSend])
4553

4654
// Fetch roles on component mount
4755
useEffect(() => {
@@ -85,6 +93,11 @@ export const UsersPage: React.FC = () => {
8593
isLoading={isLoading}
8694
canEditUsers={canEditUsers}
8795
canCreateUser={canCreateUser}
96+
filter={usersState.context.filter}
97+
onFilter={(query) => {
98+
searchParams.set("filter", query)
99+
setSearchParams(searchParams)
100+
}}
88101
/>
89102

90103
<ConfirmDialog

site/src/pages/UsersPage/UsersPageView.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@ import * as TypesGen from "../../api/typesGenerated"
55
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
66
import { Margins } from "../../components/Margins/Margins"
77
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
8+
import { SearchBarWithFilter } from "../../components/SearchBarWithFilter/SearchBarWithFilter"
89
import { UsersTable } from "../../components/UsersTable/UsersTable"
10+
import { userFilterQuery } from "../../util/filters"
911

1012
export const Language = {
1113
pageTitle: "Users",
1214
createButton: "New user",
15+
activeUsersFilterName: "Active users",
16+
allUsersFilterName: "All users",
1317
}
1418

1519
export interface UsersPageViewProps {
1620
users?: TypesGen.User[]
1721
roles?: TypesGen.Role[]
22+
filter?: string
1823
error?: unknown
1924
isUpdatingUserRoles?: boolean
2025
canEditUsers?: boolean
@@ -25,6 +30,7 @@ export interface UsersPageViewProps {
2530
onActivateUser: (user: TypesGen.User) => void
2631
onResetUserPassword: (user: TypesGen.User) => void
2732
onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void
33+
onFilter: (query: string) => void
2834
}
2935

3036
export const UsersPageView: FC<UsersPageViewProps> = ({
@@ -40,7 +46,14 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
4046
canEditUsers,
4147
canCreateUser,
4248
isLoading,
49+
filter,
50+
onFilter,
4351
}) => {
52+
const presetFilters = [
53+
{ query: userFilterQuery.active, name: Language.activeUsersFilterName },
54+
{ query: userFilterQuery.all, name: Language.allUsersFilterName },
55+
]
56+
4457
return (
4558
<Margins>
4659
<PageHeader
@@ -55,6 +68,8 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
5568
<PageHeaderTitle>Users</PageHeaderTitle>
5669
</PageHeader>
5770

71+
<SearchBarWithFilter filter={filter} onFilter={onFilter} presetFilters={presetFilters} />
72+
5873
{error ? (
5974
<ErrorSummary error={error} />
6075
) : (

site/src/pages/WorkspacesPage/WorkspacesPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { useMachine } from "@xstate/react"
22
import { FC, useEffect } from "react"
33
import { Helmet } from "react-helmet"
44
import { useSearchParams } from "react-router-dom"
5+
import { workspaceFilterQuery } from "../../util/filters"
56
import { pageTitle } from "../../util/page"
6-
import { workspaceFilterQuery } from "../../util/workspace"
77
import { workspacesMachine } from "../../xServices/workspaces/workspacesXService"
88
import { WorkspacesPageView } from "./WorkspacesPageView"
99

site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ComponentMeta, Story } from "@storybook/react"
22
import { spawn } from "xstate"
33
import { ProvisionerJobStatus, WorkspaceTransition } from "../../api/typesGenerated"
44
import { MockWorkspace } from "../../testHelpers/entities"
5-
import { workspaceFilterQuery } from "../../util/workspace"
5+
import { workspaceFilterQuery } from "../../util/filters"
66
import { workspaceItemMachine, WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
77
import { WorkspacesPageView, WorkspacesPageViewProps } from "./WorkspacesPageView"
88

site/src/pages/WorkspacesPage/WorkspacesPageView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ import {
3131
HelpTooltipText,
3232
HelpTooltipTitle,
3333
} from "../../components/Tooltips/HelpTooltip/HelpTooltip"
34-
import { getDisplayStatus, workspaceFilterQuery } from "../../util/workspace"
34+
import { workspaceFilterQuery } from "../../util/filters"
35+
import { getDisplayStatus } from "../../util/workspace"
3536
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
3637

3738
dayjs.extend(relativeTime)

site/src/util/filters.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as TypesGen from "../api/typesGenerated"
2+
import { queryToFilter } from "./filters"
3+
4+
describe("queryToFilter", () => {
5+
it.each<[string | undefined, TypesGen.WorkspaceFilter | TypesGen.UsersRequest]>([
6+
[undefined, {}],
7+
["", { q: "" }],
8+
["asdkfvjn", { q: "asdkfvjn" }],
9+
["owner:me", { q: "owner:me" }],
10+
["owner:me owner:me2", { q: "owner:me owner:me2" }],
11+
["me/dev", { q: "me/dev" }],
12+
["me/", { q: "me/" }],
13+
[" key:val owner:me ", { q: "key:val owner:me" }],
14+
])(`query=%p, filter=%p`, (query, filter) => {
15+
expect(queryToFilter(query)).toEqual(filter)
16+
})
17+
})

site/src/util/filters.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as TypesGen from "../api/typesGenerated"
2+
3+
export const queryToFilter = (query?: string): TypesGen.WorkspaceFilter | TypesGen.UsersRequest => {
4+
const preparedQuery = query?.trim().replace(/ +/g, " ")
5+
return {
6+
q: preparedQuery,
7+
}
8+
}
9+
10+
export const workspaceFilterQuery = {
11+
me: "owner:me",
12+
all: "",
13+
}
14+
15+
export const userFilterQuery = {
16+
active: "status:active",
17+
all: "",
18+
}

site/src/util/workspace.test.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dayjs from "dayjs"
22
import * as TypesGen from "../api/typesGenerated"
33
import * as Mocks from "../testHelpers/entities"
4-
import { defaultWorkspaceExtension, isWorkspaceDeleted, isWorkspaceOn, workspaceQueryToFilter } from "./workspace"
4+
import { defaultWorkspaceExtension, isWorkspaceDeleted, isWorkspaceOn } from "./workspace"
55

66
describe("util > workspace", () => {
77
describe("isWorkspaceOn", () => {
@@ -101,18 +101,4 @@ describe("util > workspace", () => {
101101
expect(defaultWorkspaceExtension(dayjs(startTime))).toEqual(request)
102102
})
103103
})
104-
describe("workspaceQueryToFilter", () => {
105-
it.each<[string | undefined, TypesGen.WorkspaceFilter]>([
106-
[undefined, {}],
107-
["", { q: "" }],
108-
["asdkfvjn", { q: "asdkfvjn" }],
109-
["owner:me", { q: "owner:me" }],
110-
["owner:me owner:me2", { q: "owner:me owner:me2" }],
111-
["me/dev", { q: "me/dev" }],
112-
["me/", { q: "me/" }],
113-
[" key:val owner:me ", { q: "key:val owner:me" }],
114-
])(`query=%p, filter=%p`, (query, filter) => {
115-
expect(workspaceQueryToFilter(query)).toEqual(filter)
116-
})
117-
})
118104
})

site/src/util/workspace.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -292,15 +292,3 @@ export const defaultWorkspaceExtension = (__startDate?: dayjs.Dayjs): TypesGen.P
292292
deadline: fourHoursFromNow.format(),
293293
}
294294
}
295-
296-
export const workspaceQueryToFilter = (query?: string): TypesGen.WorkspaceFilter => {
297-
const preparedQuery = query?.trim().replace(/ +/g, " ")
298-
return {
299-
q: preparedQuery,
300-
}
301-
}
302-
303-
export const workspaceFilterQuery = {
304-
me: "owner:me",
305-
all: "",
306-
}

site/src/xServices/users/usersXService.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "../../api/errors"
1111
import * as TypesGen from "../../api/typesGenerated"
1212
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
13+
import { queryToFilter } from "../../util/filters"
1314
import { generateRandomString } from "../../util/random"
1415

1516
export const Language = {
@@ -28,6 +29,7 @@ export const Language = {
2829
export interface UsersContext {
2930
// Get users
3031
users?: TypesGen.User[]
32+
filter?: string
3133
getUsersError?: Error | unknown
3234
createUserErrorMessage?: string
3335
createUserFormErrors?: FieldErrors
@@ -47,7 +49,7 @@ export interface UsersContext {
4749
}
4850

4951
export type UsersEvent =
50-
| { type: "GET_USERS" }
52+
| { type: "GET_USERS"; query: string }
5153
| { type: "CREATE"; user: TypesGen.CreateUserRequest }
5254
| { type: "CANCEL_CREATE_USER" }
5355
// Suspend events
@@ -97,7 +99,10 @@ export const usersMachine = createMachine(
9799
states: {
98100
idle: {
99101
on: {
100-
GET_USERS: "gettingUsers",
102+
GET_USERS: {
103+
actions: "assignFilter",
104+
target: "gettingUsers",
105+
},
101106
CREATE: "creatingUser",
102107
CANCEL_CREATE_USER: { actions: ["clearCreateUserError"] },
103108
SUSPEND_USER: {
@@ -242,7 +247,10 @@ export const usersMachine = createMachine(
242247
},
243248
error: {
244249
on: {
245-
GET_USERS: "gettingUsers",
250+
GET_USERS: {
251+
actions: "assignFilter",
252+
target: "gettingUsers",
253+
},
246254
},
247255
},
248256
},
@@ -252,7 +260,7 @@ export const usersMachine = createMachine(
252260
// Passing API.getUsers directly does not invoke the function properly
253261
// when it is mocked. This happen in the UsersPage tests inside of the
254262
// "shows a success message and refresh the page" test case.
255-
getUsers: () => API.getUsers(),
263+
getUsers: (context) => API.getUsers(queryToFilter(context.filter)),
256264
createUser: (_, event) => API.createUser(event.user),
257265
suspendUser: (context) => {
258266
if (!context.userIdToSuspend) {
@@ -297,6 +305,9 @@ export const usersMachine = createMachine(
297305
assignUsers: assign({
298306
users: (_, event) => event.data,
299307
}),
308+
assignFilter: assign({
309+
filter: (_, event) => event.query,
310+
}),
300311
assignGetUsersError: assign({
301312
getUsersError: (_, event) => event.data,
302313
}),

site/src/xServices/workspaces/workspacesXService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as API from "../../api/api"
33
import { getErrorMessage } from "../../api/errors"
44
import * as TypesGen from "../../api/typesGenerated"
55
import { displayError, displayMsg, displaySuccess } from "../../components/GlobalSnackbar/utils"
6-
import { workspaceQueryToFilter } from "../../util/workspace"
6+
import { queryToFilter } from "../../util/filters"
77

88
/**
99
* Workspace item machine
@@ -316,7 +316,7 @@ export const workspacesMachine = createMachine(
316316
}),
317317
},
318318
services: {
319-
getWorkspaces: (context) => API.getWorkspaces(workspaceQueryToFilter(context.filter)),
319+
getWorkspaces: (context) => API.getWorkspaces(queryToFilter(context.filter)),
320320
},
321321
},
322322
)

0 commit comments

Comments
 (0)