feat(site): add presets back to the filters (#7876)

This commit is contained in:
Bruno Quaresma
2023-06-07 09:46:16 -03:00
committed by GitHub
parent a77b48a5e3
commit 91dd3fbfab
6 changed files with 181 additions and 50 deletions

View File

@ -23,6 +23,13 @@ import { BaseOption } from "./options"
import debounce from "just-debounce-it" import debounce from "just-debounce-it"
import MenuList from "@mui/material/MenuList" import MenuList from "@mui/material/MenuList"
import { Loader } from "components/Loader/Loader" import { Loader } from "components/Loader/Loader"
import Divider from "@mui/material/Divider"
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"
export type PresetFilter = {
name: string
query: string
}
type FilterValues = Record<string, string | undefined> type FilterValues = Record<string, string | undefined>
@ -127,12 +134,16 @@ export const Filter = ({
error, error,
skeleton, skeleton,
options, options,
learnMoreLink,
presets,
}: { }: {
filter: ReturnType<typeof useFilter> filter: ReturnType<typeof useFilter>
skeleton: ReactNode skeleton: ReactNode
isLoading: boolean isLoading: boolean
learnMoreLink: string
error?: unknown error?: unknown
options?: ReactNode options?: ReactNode
presets: PresetFilter[]
}) => { }) => {
const shouldDisplayError = hasError(error) && isApiValidationError(error) const shouldDisplayError = hasError(error) && isApiValidationError(error)
const hasFilterQuery = filter.query !== "" const hasFilterQuery = filter.query !== ""
@ -148,54 +159,71 @@ export const Filter = ({
skeleton skeleton
) : ( ) : (
<> <>
<TextField <Box sx={{ display: "flex", width: "100%" }}>
fullWidth <PresetMenu
error={shouldDisplayError} onSelect={(query) => filter.update(query)}
helperText={ presets={presets}
shouldDisplayError ? getValidationErrorMessage(error) : undefined learnMoreLink={learnMoreLink}
} />
size="small" <TextField
InputProps={{ fullWidth
name: "query", error={shouldDisplayError}
placeholder: "Search...", helperText={
value: searchQuery, shouldDisplayError
onChange: (e) => { ? getValidationErrorMessage(error)
setSearchQuery(e.target.value) : undefined
filter.debounceUpdate(e.target.value) }
}, size="small"
sx: { InputProps={{
borderRadius: "6px", name: "query",
"& input::placeholder": { placeholder: "Search...",
color: (theme) => theme.palette.text.secondary, value: searchQuery,
onChange: (e) => {
setSearchQuery(e.target.value)
filter.debounceUpdate(e.target.value)
}, },
}, sx: {
startAdornment: ( borderRadius: "6px",
<InputAdornment position="start"> borderTopLeftRadius: 0,
<SearchOutlined borderBottomLeftRadius: 0,
sx={{ marginLeft: "-1px",
fontSize: 14, "&:hover": {
color: (theme) => theme.palette.text.secondary, zIndex: 2,
}} },
/> "& input::placeholder": {
</InputAdornment> color: (theme) => theme.palette.text.secondary,
), },
endAdornment: hasFilterQuery && ( "& .MuiInputAdornment-root": {
<InputAdornment position="end"> marginLeft: 0,
<Tooltip title="Clear filter"> },
<IconButton },
size="small" startAdornment: (
onClick={() => { <InputAdornment position="start">
filter.update("") <SearchOutlined
sx={{
fontSize: 14,
color: (theme) => theme.palette.text.secondary,
}} }}
> />
<CloseOutlined sx={{ fontSize: 14 }} /> </InputAdornment>
</IconButton> ),
</Tooltip> endAdornment: hasFilterQuery && (
</InputAdornment> <InputAdornment position="end">
), <Tooltip title="Clear filter">
}} <IconButton
/> size="small"
onClick={() => {
filter.update("")
}}
>
<CloseOutlined sx={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>
</Box>
{options} {options}
</> </>
)} )}
@ -203,6 +231,78 @@ export const Filter = ({
) )
} }
const PresetMenu = ({
presets,
learnMoreLink,
onSelect,
}: {
presets: PresetFilter[]
learnMoreLink: string
onSelect: (query: string) => void
}) => {
const [isOpen, setIsOpen] = useState(false)
const anchorRef = useRef<HTMLButtonElement>(null)
return (
<>
<Button
onClick={() => setIsOpen(true)}
ref={anchorRef}
sx={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
flexShrink: 0,
zIndex: 1,
}}
endIcon={<KeyboardArrowDown />}
>
Filters
</Button>
<Menu
id="filter-menu"
anchorEl={anchorRef.current}
open={isOpen}
onClose={() => setIsOpen(false)}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
sx={{ "& .MuiMenu-paper": { py: 1 } }}
>
{presets.map((presetFilter) => (
<MenuItem
sx={{ fontSize: 14 }}
key={presetFilter.name}
onClick={() => {
onSelect(presetFilter.query)
setIsOpen(false)
}}
>
{presetFilter.name}
</MenuItem>
))}
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
<MenuItem
component="a"
href={learnMoreLink}
target="_blank"
sx={{ fontSize: 13, fontWeight: 500 }}
onClick={() => {
setIsOpen(false)
}}
>
<OpenInNewOutlined sx={{ fontSize: "14px !important" }} />
View advanced filtering
</MenuItem>
</Menu>
</>
)
}
export const FilterMenu = <TOption extends BaseOption>({ export const FilterMenu = <TOption extends BaseOption>({
id, id,
menu, menu,

View File

@ -11,6 +11,7 @@ import {
} from "components/Filter/filter" } from "components/Filter/filter"
import { BaseOption } from "components/Filter/options" import { BaseOption } from "components/Filter/options"
import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu" import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu"
import { userFilterQuery } from "utils/filters"
type StatusOption = BaseOption & { type StatusOption = BaseOption & {
color: string color: string
@ -36,6 +37,11 @@ export const useStatusFilterMenu = ({
export type StatusFilterMenu = ReturnType<typeof useStatusFilterMenu> export type StatusFilterMenu = ReturnType<typeof useStatusFilterMenu>
const PRESET_FILTERS = [
{ query: userFilterQuery.active, name: "Active users" },
{ query: userFilterQuery.all, name: "All users" },
]
export const UsersFilter = ({ export const UsersFilter = ({
filter, filter,
error, error,
@ -49,6 +55,8 @@ export const UsersFilter = ({
}) => { }) => {
return ( return (
<Filter <Filter
presets={PRESET_FILTERS}
learnMoreLink="https://coder.com/docs/v2/latest/admin/users#user-filtering"
isLoading={menus.status.isInitializing} isLoading={menus.status.isInitializing}
filter={filter} filter={filter}
error={error} error={error}

View File

@ -4,7 +4,7 @@ import { Helmet } from "react-helmet-async"
import { pageTitle } from "utils/page" import { pageTitle } from "utils/page"
import { useWorkspacesData, useWorkspaceUpdate } from "./data" import { useWorkspacesData, useWorkspaceUpdate } from "./data"
import { WorkspacesPageView } from "./WorkspacesPageView" import { WorkspacesPageView } from "./WorkspacesPageView"
import { useMe, useOrganizationId, usePermissions } from "hooks" import { useOrganizationId, usePermissions } from "hooks"
import { import {
useUserFilterMenu, useUserFilterMenu,
useTemplateFilterMenu, useTemplateFilterMenu,
@ -15,7 +15,6 @@ import { useDashboard } from "components/Dashboard/DashboardProvider"
import { useFilter } from "components/Filter/filter" import { useFilter } from "components/Filter/filter"
const WorkspacesPage: FC = () => { const WorkspacesPage: FC = () => {
const me = useMe()
const orgId = useOrganizationId() const orgId = useOrganizationId()
// If we use a useSearchParams for each hook, the values will not be in sync. // If we use a useSearchParams for each hook, the values will not be in sync.
// So we have to use a single one, centralizing the values, and pass it to // So we have to use a single one, centralizing the values, and pass it to
@ -23,7 +22,7 @@ const WorkspacesPage: FC = () => {
const searchParamsResult = useSearchParams() const searchParamsResult = useSearchParams()
const pagination = usePagination({ searchParamsResult }) const pagination = usePagination({ searchParamsResult })
const filter = useFilter({ const filter = useFilter({
initialValue: `owner:${me.username}`, initialValue: `owner:me`,
searchParamsResult, searchParamsResult,
onUpdate: () => { onUpdate: () => {
pagination.goToPage(1) pagination.goToPage(1)

View File

@ -84,7 +84,7 @@ const mockMenu = {
const defaultFilterProps = { const defaultFilterProps = {
filter: { filter: {
query: `owner:${MockUser.username}`, query: `owner:me`,
update: () => action("update"), update: () => action("update"),
debounceUpdate: action("debounce") as any, debounceUpdate: action("debounce") as any,
used: false, used: false,

View File

@ -14,6 +14,20 @@ import {
SearchFieldSkeleton, SearchFieldSkeleton,
useFilter, useFilter,
} from "components/Filter/filter" } from "components/Filter/filter"
import { workspaceFilterQuery } from "utils/filters"
const PRESET_FILTERS = [
{ query: workspaceFilterQuery.me, name: "My workspaces" },
{ query: workspaceFilterQuery.all, name: "All workspaces" },
{
query: workspaceFilterQuery.running,
name: "Running workspaces",
},
{
query: workspaceFilterQuery.failed,
name: "Failed workspaces",
},
]
export const WorkspacesFilter = ({ export const WorkspacesFilter = ({
filter, filter,
@ -30,9 +44,11 @@ export const WorkspacesFilter = ({
}) => { }) => {
return ( return (
<Filter <Filter
presets={PRESET_FILTERS}
isLoading={menus.status.isInitializing} isLoading={menus.status.isInitializing}
filter={filter} filter={filter}
error={error} error={error}
learnMoreLink="https://coder.com/docs/v2/latest/workspaces#workspace-filtering"
options={ options={
<> <>
{menus.user && <UserMenu {...menus.user} />} {menus.user && <UserMenu {...menus.user} />}

View File

@ -29,6 +29,14 @@ export const useUserFilterMenu = ({
value, value,
id: "owner", id: "owner",
getSelectedOption: async () => { getSelectedOption: async () => {
if (value === "me") {
return {
label: me.username,
value: me.username,
avatarUrl: me.avatar_url,
}
}
const usersRes = await getUsers({ q: value, limit: 1 }) const usersRes = await getUsers({ q: value, limit: 1 })
const firstUser = usersRes.users.at(0) const firstUser = usersRes.users.at(0)
if (firstUser && firstUser.username === value) { if (firstUser && firstUser.username === value) {