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 MenuList from "@mui/material/MenuList"
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>
@ -127,12 +134,16 @@ export const Filter = ({
error,
skeleton,
options,
learnMoreLink,
presets,
}: {
filter: ReturnType<typeof useFilter>
skeleton: ReactNode
isLoading: boolean
learnMoreLink: string
error?: unknown
options?: ReactNode
presets: PresetFilter[]
}) => {
const shouldDisplayError = hasError(error) && isApiValidationError(error)
const hasFilterQuery = filter.query !== ""
@ -148,54 +159,71 @@ export const Filter = ({
skeleton
) : (
<>
<TextField
fullWidth
error={shouldDisplayError}
helperText={
shouldDisplayError ? getValidationErrorMessage(error) : undefined
}
size="small"
InputProps={{
name: "query",
placeholder: "Search...",
value: searchQuery,
onChange: (e) => {
setSearchQuery(e.target.value)
filter.debounceUpdate(e.target.value)
},
sx: {
borderRadius: "6px",
"& input::placeholder": {
color: (theme) => theme.palette.text.secondary,
<Box sx={{ display: "flex", width: "100%" }}>
<PresetMenu
onSelect={(query) => filter.update(query)}
presets={presets}
learnMoreLink={learnMoreLink}
/>
<TextField
fullWidth
error={shouldDisplayError}
helperText={
shouldDisplayError
? getValidationErrorMessage(error)
: undefined
}
size="small"
InputProps={{
name: "query",
placeholder: "Search...",
value: searchQuery,
onChange: (e) => {
setSearchQuery(e.target.value)
filter.debounceUpdate(e.target.value)
},
},
startAdornment: (
<InputAdornment position="start">
<SearchOutlined
sx={{
fontSize: 14,
color: (theme) => theme.palette.text.secondary,
}}
/>
</InputAdornment>
),
endAdornment: hasFilterQuery && (
<InputAdornment position="end">
<Tooltip title="Clear filter">
<IconButton
size="small"
onClick={() => {
filter.update("")
sx: {
borderRadius: "6px",
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
marginLeft: "-1px",
"&:hover": {
zIndex: 2,
},
"& input::placeholder": {
color: (theme) => theme.palette.text.secondary,
},
"& .MuiInputAdornment-root": {
marginLeft: 0,
},
},
startAdornment: (
<InputAdornment position="start">
<SearchOutlined
sx={{
fontSize: 14,
color: (theme) => theme.palette.text.secondary,
}}
>
<CloseOutlined sx={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>
/>
</InputAdornment>
),
endAdornment: hasFilterQuery && (
<InputAdornment position="end">
<Tooltip title="Clear filter">
<IconButton
size="small"
onClick={() => {
filter.update("")
}}
>
<CloseOutlined sx={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>
</Box>
{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>({
id,
menu,

View File

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

View File

@ -4,7 +4,7 @@ import { Helmet } from "react-helmet-async"
import { pageTitle } from "utils/page"
import { useWorkspacesData, useWorkspaceUpdate } from "./data"
import { WorkspacesPageView } from "./WorkspacesPageView"
import { useMe, useOrganizationId, usePermissions } from "hooks"
import { useOrganizationId, usePermissions } from "hooks"
import {
useUserFilterMenu,
useTemplateFilterMenu,
@ -15,7 +15,6 @@ import { useDashboard } from "components/Dashboard/DashboardProvider"
import { useFilter } from "components/Filter/filter"
const WorkspacesPage: FC = () => {
const me = useMe()
const orgId = useOrganizationId()
// 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
@ -23,7 +22,7 @@ const WorkspacesPage: FC = () => {
const searchParamsResult = useSearchParams()
const pagination = usePagination({ searchParamsResult })
const filter = useFilter({
initialValue: `owner:${me.username}`,
initialValue: `owner:me`,
searchParamsResult,
onUpdate: () => {
pagination.goToPage(1)

View File

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

View File

@ -14,6 +14,20 @@ import {
SearchFieldSkeleton,
useFilter,
} 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 = ({
filter,
@ -30,9 +44,11 @@ export const WorkspacesFilter = ({
}) => {
return (
<Filter
presets={PRESET_FILTERS}
isLoading={menus.status.isInitializing}
filter={filter}
error={error}
learnMoreLink="https://coder.com/docs/v2/latest/workspaces#workspace-filtering"
options={
<>
{menus.user && <UserMenu {...menus.user} />}

View File

@ -29,6 +29,14 @@ export const useUserFilterMenu = ({
value,
id: "owner",
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 firstUser = usersRes.users.at(0)
if (firstUser && firstUser.username === value) {