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+
}

0 commit comments

Comments
 (0)