8000 feat(site): add new filter to the users page by BrunoQuaresma · Pull Request #7818 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat(site): add new filter to the users page #7818

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 7 commits into from
Jun 6, 2023
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 basic filtering to users
  • Loading branch information
BrunoQuaresma committed Jun 2, 2023
commit fdcc802243d09ae23f6c09c00f08088671b17924
44 changes: 44 additions & 0 deletions site/src/components/PaginationStatus/PaginationStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Box from "@mui/material/Box"
import Skeleton from "@mui/material/Skeleton"

type BasePaginationStatusProps = {
label: string
isLoading: boolean
showing?: number
total?: number
}

type LoadedPaginationStatusProps = BasePaginationStatusProps & {
isLoading: false
showing: number
total: number
}

export const PaginationStatus = ({
isLoading,
showing,
total,
label,
}: BasePaginationStatusProps | LoadedPaginationStatusProps) => {
return (
<Box
sx={{
fontSize: 13,
mb: 2,
mt: 1,
color: (theme) => theme.palette.text.secondary,
"& strong": { color: (theme) => theme.palette.text.primary },
}}
>
{!isLoading ? (
<>
Showing <strong>{showing}</strong> of <strong>{total}</strong> {label}
</>
) : (
<Box sx={{ height: 24, display: "flex", alignItems: "center" }}>
<Skeleton variant="text" width={160} height={16} />
</Box>
)}
</Box>
)
}
47 changes: 41 additions & 6 deletions site/src/pages/UsersPage/UsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "components/PaginationWidget/utils"
import { useMe } from "hooks/useMe"
import { usePermissions } from "hooks/usePermissions"
import { FC, ReactNode } from "react"
import { FC, ReactNode, useEffect } from "react"
import { Helmet } from "react-helmet-async"
import { useNavigate } from "react-router"
import { useSearchParams } from "react-router-dom"
Expand All @@ -17,6 +17,9 @@ import { ConfirmDialog } from "../../components/Dialogs/ConfirmDialog/ConfirmDia
import { ResetPasswordDialog } from "../../components/Dialogs/ResetPasswordDialog/ResetPasswordDialog"
import { pageTitle } from "../../utils/page"
import { UsersPageView } from "./UsersPageView"
import { useFilter } from "./filter/filter"
import { useStatusAutocomplete } from "./filter/autocompletes"
import { useDashboard } from "components/Dashboard/DashboardProvider"

export const Language = {
suspendDialogTitle: "Suspend user",
Expand All @@ -32,7 +35,8 @@ const getSelectedUser = (id: string, users?: User[]) =>

export const UsersPage: FC<{ children?: ReactNode }> = () => {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const searchParamsResult = useSearchParams()
const [searchParams, setSearchParams] = searchParamsResult
const filter = searchParams.get("filter") ?? ""
const [usersState, usersSend] = useMachine(usersMachine, {
context: {
Expand Down Expand Up @@ -73,6 +77,26 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {

const me = useMe()

// New filter
const dashboard = useDashboard()
const useFilterResult = useFilter({
searchParamsResult,
onUpdate: () => {
usersSend({ type: "UPDATE_PAGE", page: "1" })
},
})
useEffect(() => {
usersSend({ type: "UPDATE_FILTER", query: useFilterResult.query })
}, [useFilterResult.query, usersSend])
const statusAutocomplete = useStatusAutocomplete(
useFilterResult.values.status,
(option) =>
useFilterResult.update({
...useFilterResult.values,
status: option?.value,
}),
)

return (
<>
<Helmet>
Expand Down Expand Up @@ -123,13 +147,24 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
isUpdatingUserRoles={usersState.matches("updatingUserRoles")}
isLoading={isLoading}
canEditUsers={canEditUsers}
filter={usersState.context.filter}
onFilter={(query) => {
usersSend({ type: "UPDATE_FILTER", query })
}}
paginationRef={paginationRef}
isNonInitialPage={nonInitialPage(searchParams)}
actorID={me.id}
filterProps={
dashboard.experiments.includes("workspace_filter")
? {
filter: useFilterResult,
autocomplete: {
status: statusAutocomplete,
},
}
: {
filter: usersState.context.filter,
onFilter: (query) => {
usersSend({ type: "UPDATE_FILTER", query })
},
}
}
/>

<DeleteDialog
Expand Down
32 changes: 22 additions & 10 deletions site/src/pages/UsersPage/UsersPageView.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { PaginationWidget } from "components/PaginationWidget/PaginationWidget"
import { FC } from "react"
import { ComponentProps, FC } from "react"
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
import * as TypesGen from "../../api/typesGenerated"
import { SearchBarWithFilter } from "../../components/SearchBarWithFilter/SearchBarWithFilter"
import { UsersTable } from "../../components/UsersTable/UsersTable"
import { userFilterQuery } from "../../utils/filters"
import { Filter } from "./filter/filter"
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"

export const Language = {
activeUsersFilterName: "Active users",
Expand All @@ -14,7 +16,6 @@ export interface UsersPageViewProps {
users?: TypesGen.User[]
count?: number
roles?: TypesGen.AssignableRoles[]
filter?: string
error?: unknown
isUpdatingUserRoles?: boolean
canEditUsers?: boolean
Expand All @@ -28,7 +29,9 @@ export interface UsersPageViewProps {
user: TypesGen.User,
roles: TypesGen.Role["name"][],
) => void
onFilter: (query: string) => void
filterProps:
| ComponentProps<typeof SearchBarWithFilter>
| ComponentProps<typeof Filter>
paginationRef: PaginationMachineRef
isNonInitialPage: boolean
actorID: string
Expand All @@ -48,8 +51,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
isUpdatingUserRoles,
canEditUsers,
isLoading,
filter,
onFilter,
filterProps,
paginationRef,
isNonInitialPage,
actorID,
Expand All @@ -61,11 +63,21 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({

return (
<>
<SearchBarWithFilter
filter={filter}
onFilter={onFilter}
presetFilters={presetFilters}
error={error}
{"onFilter" in filterProps ? (
<SearchBarWithFilter
{...filterProps}
presetFilters={presetFilters}
error={error}
/>
) : (
<Filter {...filterProps} />
)}

<PaginationStatus
isLoading={Boolean(isLoading)}
showing={users?.length}
total={count}
label="users"
/>

<UsersTable
Expand Down
123 changes: 123 additions & 0 deletions site/src/pages/UsersPage/filter/autocompletes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useMemo, useRef, useState } from "react"
import { BaseOption, StatusOption } from "./options"
import { useQuery } from "@tanstack/react-query"

type UseAutocompleteOptions<TOption extends BaseOption> = {
id: string
value: string | undefined
// Using null because of react-query
// https://tanstack.com/query/v4/docs/react/guides/migrating-to-react-query-4#undefined-is-an-illegal-cache-value-for-successful-queries
getSelectedOption: () => Promise<TOption | null>
getOptions: (query: string) => Promise<TOption[]>
onChange: (option: TOption | undefined) => void
enabled?: boolean
}

const useAutocomplete = <TOption extends BaseOption = BaseOption>({
id,
value,
getSelectedOption,
getOptions,
onChange,
enabled,
}: UseAutocompleteOptions<TOption>) => {
const selectedOptionsCacheRef = useRef<Record<string, TOption>>({})
const [query, setQuery] = useState("")
const selectedOptionQuery = useQuery({
queryKey: [id, "autocomplete", "selected", value],
queryFn: () => {
if (!value) {
return null
}

const cachedOption = selectedOptionsCacheRef.current[value]
if (cachedOption) {
return cachedOption
}

return getSelectedOption()
},
enabled,
keepPreviousData: true,
})
const selectedOption = selectedOptionQuery.data
const searchOptionsQuery = useQuery({
queryKey: [id, "autocomplete", "search", query],
queryFn: () => getOptions(query),
enabled,
})
const searchOptions = useMemo(() => {
const isDataLoaded =
searchOptionsQuery.isFetched && selectedOptionQuery.isFetched

if (!isDataLoaded) {
return undefined
}

let options = searchOptionsQuery.data ?? []

if (selectedOption) {
options = options.filter(
(option) => option.value !== selectedOption.value,
)
options = [selectedOption, ...options]
}

options = options.filter(
(option) =>
option.label.toLowerCase().includes(query.toLowerCase()) ||
option.value.toLowerCase().includes(query.toLowerCase()),
)

return options
}, [
selectedOptionQuery.isFetched,
query,
searchOptionsQuery.data,
searchOptionsQuery.isFetched,
selectedOption,
])

const selectOption = (option: TOption) => {
let newSelectedOptionValue: TOption | undefined = option
selectedOptionsCacheRef.current[option.value] = option
setQuery("")

if (option.value === selectedOption?.value) {
newSelectedOptionValue = undefined
}

onChange(newSelectedOptionValue)
}

return {
query,
setQuery,
selectedOption,
selectOption,
searchOptions,
isInitializing: selectedOptionQuery.isInitialLoading,
initialOption: selectedOptionQuery.data,
isSearching: searchOptionsQuery.isFetching,
}
}

export const useStatusAutocomplete = (
value: string | undefined,
onChange: (option: StatusOption | undefined) => void,
) => {
const statusOptions: StatusOption[] = [
{ value: "active", label: "Active", color: "success" },
{ value: "suspended", label: "Suspended", color: "secondary" },
]
return useAutocomplete({
onChange,
value,
id: "status",
getSelectedOption: async () =>
statusOptions.find((option) => option.value === value) ?? null,
getOptions: async () => statusOptions,
})
}

export type StatusAutocomplete = ReturnType<typeof useStatusAutocomplete>
Loading
0