8000 feat: Add filter on Users page by AbhineetJain · Pull Request #2653 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: Add filter on Users page #2653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add filter search on Users page
  • Loading branch information
AbhineetJain committed Jun 28, 2022
commit acbd54ab93762867b67c499e1546efce8ef9fd97
21 changes: 12 additions & 9 deletions site/src/api/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from "axios"
import { getApiKey, getWorkspacesURL, login, logout } from "./api"
import { getApiKey, getURLWithSearchParams, login, logout } from "./api"
import * as TypesGen from "./typesGenerated"

describe("api.ts", () => {
Expand Down Expand Up @@ -114,16 +114,19 @@ describe("api.ts", () => {
})
})

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

[{ q: "" }, "/api/v2/workspaces"],
[{ q: "owner:1" }, "/api/v2/workspaces?q=owner%3A1"],
["/api/v2/workspaces", { q: "" }, "/api/v2/workspaces"],
["/api/v2/workspaces", { q: "owner:1" }, "/api/v2/workspaces?q=owner%3A1"],

[{ q: "owner:me" }, "/api/v2/workspaces?q=owner%3Ame"],
])(`getWorkspacesURL(%p) returns %p`, (filter, expected) => {
expect(getWorkspacesURL(filter)).toBe(expected)
["/api/v2/workspaces", { q: "owner:me" }, "/api/v2/workspaces?q=owner%3Ame"],

["/api/v2/users", { q: "status:active" }, "/api/v2/users?q=status%3Aactive"],
["/api/v2/users", { q: "" }, "/api/v2/users"],
])(`getURLWithSearchParams(%p) returns %p`, (basePath, filter, expected) => {
expect(getURLWithSearchParams(basePath, filter)).toBe(expected)
})
})
})
13 changes: 8 additions & 5 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
return response.data
}

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

Expand Down Expand Up @@ -144,8 +145,10 @@ export const getWorkspace = async (
return response.data
}

export const getWorkspacesURL = (filter?: TypesGen.WorkspaceFilter): string => {
const basePath = "/api/v2/workspaces"
export const getURLWithSearchParams = (
basePath: string,
filter?: TypesGen.WorkspaceFilter | TypesGen.UsersRequest,
): string => {
const searchParams = new URLSearchParams()

if (filter?.q && filter.q !== "") {
Expand All @@ -160,7 +163,7 @@ export const getWorkspacesURL = (filter?: TypesGen.WorkspaceFilter): string => {
export const getWorkspaces = async (
filter?: TypesGen.WorkspaceFilter,
): Promise<TypesGen.Workspace[]> => {
const url = getWorkspacesURL(filter)
const url = getURLWithSearchParams("/api/v2/workspaces", filter)
const response = await axios.get<TypesGen.Workspace[]>(url)
return response.data
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ComponentMeta, Story } from "@storybook/react"
import { workspaceFilterQuery } from "../../util/workspace"
import { workspaceFilterQuery } from "../../util/filters"
import { SearchBarWithFilter, SearchBarWithFilterProps } from "./SearchBarWithFilter"

export default {
Expand Down
17 changes: 15 additions & 2 deletions site/src/pages/UsersPage/UsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { useActor, useSelector } from "@xstate/react"
import React, { useContext, useEffect } from "react"
import { Helmet } from "react-helmet"
import { useNavigate } from "react-router"
import { useSearchParams } from "react-router-dom"
import { ConfirmDialog } from "../../components/ConfirmDialog/ConfirmDialog"
import { ResetPasswordDialog } from "../../components/ResetPasswordDialog/ResetPasswordDialog"
import { userFilterQuery } from "../../util/filters"
import { pageTitle } from "../../util/page"
import { selectPermissions } from "../../xServices/auth/authSelectors"
import { XServiceContext } from "../../xServices/StateContext"
Expand Down Expand Up @@ -31,6 +33,7 @@ export const UsersPage: React.FC = () => {
newUserPassword,
} = usersState.context
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
const userToBeActivated = users?.find((u) => u.id === userIdToActivate)
const userToResetPassword = users?.find((u) => u.id === userIdToResetPassword)
Expand All @@ -46,8 +49,13 @@ export const UsersPage: React.FC = () => {

// Fetch users on component mount
useEffect(() => {
usersSend("GET_USERS")
}, [usersSend])
const filter = searchParams.get("filter")
const query = filter !== null ? filter : userFilterQuery.active
usersSend({
type: "GET_USERS",
query,
})
}, [searchParams, usersSend])

// Fetch roles on component mount
useEffect(() => {
Expand Down Expand Up @@ -91,6 +99,11 @@ export const UsersPage: React.FC = () => {
isLoading={isLoading}
canEditUsers={canEditUsers}
canCreateUser={canCreateUser}
filter={usersState.context.filter}
onFilter={(query) => {
searchParams.set("filter", query)
setSearchParams(searchParams)
}}
/>

<ConfirmDialog
Expand Down
15 changes: 15 additions & 0 deletions site/src/pages/UsersPage/UsersPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ import * as TypesGen from "../../api/typesGenerated"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
import { Margins } from "../../components/Margins/Margins"
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
import { SearchBarWithFilter } from "../../components/SearchBarWithFilter/SearchBarWithFilter"
import { UsersTable } from "../../components/UsersTable/UsersTable"
import { userFilterQuery } from "../../util/filters"

export const Language = {
pageTitle: "Users",
createButton: "New user",
activeUsersFilterName: "Active users",
allUsersFilterName: "All users",
}

export interface UsersPageViewProps {
users?: TypesGen.User[]
roles?: TypesGen.Role[]
filter?: string
error?: unknown
isUpdatingUserRoles?: boolean
canEditUsers?: boolean
Expand All @@ -25,6 +30,7 @@ export interface UsersPageViewProps {
onActivateUser: (user: TypesGen.User) => void
onResetUserPassword: (user: TypesGen.User) => void
onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void
onFilter: (query: string) => void
}

export const UsersPageView: FC<UsersPageViewProps> = ({
Expand All @@ -40,7 +46,14 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
canEditUsers,
canCreateUser,
isLoading,
filter,
onFilter,
}) => {
const presetFilters = [
{ query: userFilterQuery.active, name: Language.activeUsersFilterName },
{ query: userFilterQuery.all, name: Language.allUsersFilterName },
]

return (
<Margins>
<PageHeader
Expand All @@ -55,6 +68,8 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
<PageHeaderTitle>Users</PageHeaderTitle>
</PageHeader>

<SearchBarWithFilter filter={filter} onFilter={onFilter} presetFilters={presetFilters} />

{error ? (
<ErrorSummary error={error} />
) : (
Expand Down
2 changes: 1 addition & 1 deletion site/src/pages/WorkspacesPage/WorkspacesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { useMachine } from "@xstate/react"
import { FC, useEffect } from "react"
import { Helmet } from "react-helmet"
import { useSearchParams } from "react-router-dom"
import { workspaceFilterQuery } from "../../util/filters"
import { pageTitle } from "../../util/page"
import { workspaceFilterQuery } from "../../util/workspace"
import { workspacesMachine } from "../../xServices/workspaces/workspacesXService"
import { WorkspacesPageView } from "./WorkspacesPageView"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ComponentMeta, Story } from "@storybook/react"
import { spawn } from "xstate"
import { ProvisionerJobStatus, WorkspaceTransition } from "../../api/typesGenerated"
import { MockWorkspace } from "../../testHelpers/entities"
import { workspaceFilterQuery } from "../../util/workspace"
import { workspaceFilterQuery } from "../../util/filters"
import {
workspaceItemMachine,
WorkspaceItemMachineRef,
Expand Down
3 changes: 2 additions & 1 deletion site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import {
HelpTooltipText,
HelpTooltipTitle,
} from "../../components/Tooltips/HelpTooltip/HelpTooltip"
import { getDisplayStatus, workspaceFilterQuery } from "../../util/workspace"
import { workspaceFilterQuery } from "../../util/filters"
import { getDisplayStatus } from "../../util/workspace"
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"

dayjs.extend(relativeTime)
Expand Down
17 changes: 17 additions & 0 deletions site/src/util/filters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as TypesGen from "../api/typesGenerated"
import { queryToFilter } from "./filters"

describe("queryToFilter", () => {
it.each<[string | undefined, TypesGen.WorkspaceFilter | TypesGen.UsersRequest]>([
[undefined, {}],
["", { q: "" }],
["asdkfvjn", { q: "asdkfvjn" }],
["owner:me", { q: "owner:me" }],
["owner:me owner:me2", { q: "owner:me owner:me2" }],
["me/dev", { q: "me/dev" }],
["me/", { q: "me/" }],
[" key:val owner:me ", { q: "key:val owner:me" }],
])(`query=%p, filter=%p`, (query, filter) => {
expect(queryToFilter(query)).toEqual(filter)
})
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

18 changes: 18 additions & 0 deletions site/src/util/filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as TypesGen from "../api/typesGenerated"

export const queryToFilter = (query?: string): TypesGen.WorkspaceFilter | TypesGen.UsersRequest => {
const preparedQuery = query?.trim().replace(/ +/g, " ")
return {
q: preparedQuery,
}
}

export const workspaceFilterQuery = {
me: "owner:me",
all: "",
}

export const userFilterQuery = {
active: "status:active",
all: "",
}
21 changes: 1 addition & 20 deletions site/src/util/workspace.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import dayjs from "dayjs"
import * as TypesGen from "../api/typesGenerated"
import * as Mocks from "../testHelpers/entities"
import {
defaultWorkspaceExtension,
isWorkspaceDeleted,
isWorkspaceOn,
workspaceQueryToFilter,
} from "./workspace"
import { defaultWorkspaceExtension, isWorkspaceDeleted, isWorkspaceOn } from "./workspace"

describe("util > workspace", () => {
describe("isWorkspaceOn", () => {
Expand Down Expand Up @@ -106,18 +101,4 @@ describe("util > workspace", () => {
expect(defaultWorkspaceExtension(dayjs(startTime))).toEqual(request)
})
})
describe("workspaceQueryToFilter", () => {
it.each<[string | undefined, TypesGen.WorkspaceFilter]>([
[undefined, {}],
["", { q: "" }],
["asdkfvjn", { q: "asdkfvjn" }],
["owner:me", { q: "owner:me" }],
["owner:me owner:me2", { q: "owner:me owner:me2" }],
["me/dev", { q: "me/dev" }],
["me/", { q: "me/" }],
[" key:val owner:me ", { q: "key:val owner:me" }],
])(`query=%p, filter=%p`, (query, filter) => {
expect(workspaceQueryToFilter(query)).toEqual(filter)
})
})
})
12 changes: 0 additions & 12 deletions site/src/util/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,15 +296,3 @@ export const defaultWorkspaceExtension = (
deadline: fourHoursFromNow.format(),
}
}

export const workspaceQueryToFilter = (query?: string): TypesGen.WorkspaceFilter => {
const preparedQuery = query?.trim().replace(/ +/g, " ")
return {
q: preparedQuery,
}
}

export const workspaceFilterQuery = {
me: "owner:me",
all: "",
}
19 changes: 15 additions & 4 deletions site/src/xServices/users/usersXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "../../api/errors"
import * as TypesGen from "../../api/typesGenerated"
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
import { queryToFilter } from "../../util/filters"
import { generateRandomString } from "../../util/random"

export const Language = {
Expand All @@ -28,6 +29,7 @@ export const Language = {
export interface UsersContext {
// Get users
users?: TypesGen.User[]
filter?: string
getUsersError?: Error | unknown
createUserErrorMessage?: string
createUserFormErrors?: FieldErrors
Expand All @@ -47,7 +49,7 @@ export interface UsersContext {
}

export type UsersEvent =
| { type: "GET_USERS" }
| { type: "GET_USERS"; query: string }
| { type: "CREATE"; user: TypesGen.CreateUserRequest }
| { type: "CANCEL_CREATE_USER" }
// Suspend events
Expand Down Expand Up @@ -97,7 +99,10 @@ export const usersMachine = createMachine(
states: {
idle: {
on: {
GET_USERS: "gettingUsers",
GET_USERS: {
actions: "assignFilter",
target: "gettingUsers",
},
CREATE: "creatingUser",
CANCEL_CREATE_USER: { actions: ["clearCreateUserError"] },
SUSPEND_USER: {
Expand Down Expand Up @@ -242,7 +247,10 @@ export const usersMachine = createMachine(
},
error: {
on: {
GET_USERS: "gettingUsers",
GET_USERS: {
actions: "assignFilter",
target: "gettingUsers",
},
},
},
},
Expand All @@ -252,7 +260,7 @@ export const usersMachine = createMachine(
// Passing API.getUsers directly does not invoke the function properly
// when it is mocked. This happen in the UsersPage tests inside of the
// "shows a success message and refresh the page" test case.
getUsers: () => API.getUsers(),
getUsers: (context) => API.getUsers(queryToFilter(context.filter)),
createUser: (_, event) => API.createUser(event.user),
suspendUser: (context) => {
if (!context.userIdToSuspend) {
Expand Down Expand Up @@ -297,6 +305,9 @@ export const usersMachine = createMachine(
assignUsers: assign({
users: (_, event) => event.data,
}),
assignFilter: assign({
filter: (_, event) => event.query,
}),
assignGetUsersError: assign({
getUsersError: (_, event) => event.data,
}),
Expand Down
4 changes: 2 additions & 2 deletions site/src/xServices/workspaces/workspacesXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as API from "../../api/api"
import { getErrorMessage } from "../../api/errors"
import * as TypesGen from "../../api/typesGenerated"
import { displayError, displayMsg, displaySuccess } from "../../components/GlobalSnackbar/utils"
import { workspaceQueryToFilter } from "../../util/workspace"
import { queryToFilter } from "../../util/filters"

/**
* Workspace item machine
Expand Down Expand Up @@ -318,7 +318,7 @@ export const workspacesMachine = createMachine(
}),
},
services: {
getWorkspaces: (context) => API.getWorkspaces(workspaceQueryToFilter(context.filter)),
getWorkspaces: (context) => API.getWorkspaces(queryToFilter(context.filter)),
},
},
)
0