Skip to content

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

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 2 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
192 changes: 146 additions & 46 deletions site/src/components/Filter/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -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 !== ""
Expand All @@ -148,61 +159,150 @@ 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)
},
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,
}}
/>
</InputAdornment>
),
endAdornment: hasFilterQuery && (
<InputAdornment position="end">
<Tooltip title="Clear filter">
<IconButton
size="small"
onClick={() => {
filter.update("")
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}
</>
)}
</Box>
)
}

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,
Expand Down
8 changes: 8 additions & 0 deletions site/src/pages/UsersPage/UsersFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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}
Expand Down
5 changes: 2 additions & 3 deletions site/src/pages/WorkspacesPage/WorkspacesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -15,15 +15,14 @@ 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
// each hook.
const searchParamsResult = useSearchParams()
const pagination = usePagination({ searchParamsResult })
const filter = useFilter({
initialValue: `owner:${me.username}`,
initialValue: `owner:me`,
searchParamsResult,
onUpdate: () => {
pagination.goToPage(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions site/src/pages/WorkspacesPage/filter/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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} />}
Expand Down
8 changes: 8 additions & 0 deletions site/src/pages/WorkspacesPage/filter/menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down