Skip to content

Commit 91dd3fb

Browse files
feat(site): add presets back to the filters (#7876)
1 parent a77b48a commit 91dd3fb

File tree

6 files changed

+181
-50
lines changed

6 files changed

+181
-50
lines changed

site/src/components/Filter/filter.tsx

+146-46
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ import { BaseOption } from "./options"
2323
import debounce from "just-debounce-it"
2424
import MenuList from "@mui/material/MenuList"
2525
import { Loader } from "components/Loader/Loader"
26+
import Divider from "@mui/material/Divider"
27+
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"
28+
29+
export type PresetFilter = {
30+
name: string
31+
query: string
32+
}
2633

2734
type FilterValues = Record<string, string | undefined>
2835

@@ -127,12 +134,16 @@ export const Filter = ({
127134
error,
128135
skeleton,
129136
options,
137+
learnMoreLink,
138+
presets,
130139
}: {
131140
filter: ReturnType<typeof useFilter>
132141
skeleton: ReactNode
133142
isLoading: boolean
143+
learnMoreLink: string
134144
error?: unknown
135145
options?: ReactNode
146+
presets: PresetFilter[]
136147
}) => {
137148
const shouldDisplayError = hasError(error) && isApiValidationError(error)
138149
const hasFilterQuery = filter.query !== ""
@@ -148,61 +159,150 @@ export const Filter = ({
148159
skeleton
149160
) : (
150161
<>
151-
<TextField
152-
fullWidth
153-
error={shouldDisplayError}
154-
helperText={
155-
shouldDisplayError ? getValidationErrorMessage(error) : undefined
156-
}
157-
size="small"
158-
InputProps={{
159-
name: "query",
160-
placeholder: "Search...",
161-
value: searchQuery,
162-
onChange: (e) => {
163-
setSearchQuery(e.target.value)
164-
filter.debounceUpdate(e.target.value)
165-
},
166-
sx: {
167-
borderRadius: "6px",
168-
"& input::placeholder": {
169-
color: (theme) => theme.palette.text.secondary,
162+
<Box sx={{ display: "flex", width: "100%" }}>
163+
<PresetMenu
164+
onSelect={(query) => filter.update(query)}
165+
presets={presets}
166+
learnMoreLink={learnMoreLink}
167+
/>
168+
<TextField
169+
fullWidth
170+
error={shouldDisplayError}
171+
helperText={
172+
shouldDisplayError
173+
? getValidationErrorMessage(error)
174+
: undefined
175+
}
176+
size="small"
177+
InputProps={{
178+
name: "query",
179+
placeholder: "Search...",
180+
value: searchQuery,
181+
onChange: (e) => {
182+
setSearchQuery(e.target.value)
183+
filter.debounceUpdate(e.target.value)
184+
},
185+
sx: {
186+
borderRadius: "6px",
187+
borderTopLeftRadius: 0,
188+
borderBottomLeftRadius: 0,
189+
marginLeft: "-1px",
190+
"&:hover": {
191+
zIndex: 2,
192+
},
193+
"& input::placeholder": {
194+
color: (theme) => theme.palette.text.secondary,
195+
},
196+
"& .MuiInputAdornment-root": {
197+
marginLeft: 0,
198+
},
170199
},
171-
},
172-
startAdornment: (
173-
<InputAdornment position="start">
174-
<SearchOutlined
175-
sx={{
176-
fontSize: 14,
177-
color: (theme) => theme.palette.text.secondary,
178-
}}
179-
/>
180-
</InputAdornment>
181-
),
182-
endAdornment: hasFilterQuery && (
183-
<InputAdornment position="end">
184-
<Tooltip title="Clear filter">
185-
<IconButton
186-
size="small"
187-
onClick={() => {
188-
filter.update("")
200+
startAdornment: (
201+
<InputAdornment position="start">
202+
<SearchOutlined
203+
sx={{
204+
fontSize: 14,
205+
color: (theme) => theme.palette.text.secondary,
189206
}}
190-
>
191-
<CloseOutlined sx={{ fontSize: 14 }} />
192-
</IconButton>
193-
</Tooltip>
194-
</InputAdornment>
195-
),
196-
}}
197-
/>
198-
207+
/>
208+
</InputAdornment>
209+
),
210+
endAdornment: hasFilterQuery && (
211+
<InputAdornment position="end">
212+
<Tooltip title="Clear filter">
213+
<IconButton
214+
size="small"
215+
onClick={() => {
216+
filter.update("")
217+
}}
218+
>
219+
<CloseOutlined sx={{ fontSize: 14 }} />
220+
</IconButton>
221+
</Tooltip>
222+
</InputAdornment>
223+
),
224+
}}
225+
/>
226+
</Box>
199227
{options}
200228
</>
201229
)}
202230
</Box>
203231
)
204232
}
205233

234+
const PresetMenu = ({
235+
presets,
236+
learnMoreLink,
237+
onSelect,
238+
}: {
239+
presets: PresetFilter[]
240+
learnMoreLink: string
241+
onSelect: (query: string) => void
242+
}) => {
243+
const [isOpen, setIsOpen] = useState(false)
244+
const anchorRef = useRef<HTMLButtonElement>(null)
245+
246+
return (
247+
<>
248+
<Button
249+
onClick={() => setIsOpen(true)}
250+
ref={anchorRef}
251+
sx={{
252+
borderTopRightRadius: 0,
253+
borderBottomRightRadius: 0,
254+
flexShrink: 0,
255+
zIndex: 1,
256+
}}
257+
endIcon={<KeyboardArrowDown />}
258+
>
259+
Filters
260+
</Button>
261+
<Menu
262+
id="filter-menu"
263+
anchorEl={anchorRef.current}
264+
open={isOpen}
265+
onClose={() => setIsOpen(false)}
266+
anchorOrigin={{
267+
vertical: "bottom",
268+
horizontal: "left",
269+
}}
270+
transformOrigin={{
271+
vertical: "top",
272+
horizontal: "left",
273+
}}
274+
sx={{ "& .MuiMenu-paper": { py: 1 } }}
275+
>
276+
{presets.map((presetFilter) => (
277+
<MenuItem
278+
sx={{ fontSize: 14 }}
279+
key={presetFilter.name}
280+
onClick={() => {
281+
onSelect(presetFilter.query)
282+
setIsOpen(false)
283+
}}
284+
>
285+
{presetFilter.name}
286+
</MenuItem>
287+
))}
288+
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
289+
<MenuItem
290+
component="a"
291+
href={learnMoreLink}
292+
target="_blank"
293+
sx={{ fontSize: 13, fontWeight: 500 }}
294+
onClick={() => {
295+
setIsOpen(false)
296+
}}
297+
>
298+
<OpenInNewOutlined sx={{ fontSize: "14px !important" }} />
299+
View advanced filtering
300+
</MenuItem>
301+
</Menu>
302+
</>
303+
)
304+
}
305+
206306
export const FilterMenu = <TOption extends BaseOption>({
207307
id,
208308
menu,

site/src/pages/UsersPage/UsersFilter.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "components/Filter/filter"
1212
import { BaseOption } from "components/Filter/options"
1313
import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu"
14+
import { userFilterQuery } from "utils/filters"
1415

1516
type StatusOption = BaseOption & {
1617
color: string
@@ -36,6 +37,11 @@ export const useStatusFilterMenu = ({
3637

3738
export type StatusFilterMenu = ReturnType<typeof useStatusFilterMenu>
3839

40+
const PRESET_FILTERS = [
41+
{ query: userFilterQuery.active, name: "Active users" },
42+
{ query: userFilterQuery.all, name: "All users" },
43+
]
44+
3945
export const UsersFilter = ({
4046
filter,
4147
error,
@@ -49,6 +55,8 @@ export const UsersFilter = ({
4955
}) => {
5056
return (
5157
<Filter
58+
presets={PRESET_FILTERS}
59+
learnMoreLink="https://coder.com/docs/v2/latest/admin/users#user-filtering"
5260
isLoading={menus.status.isInitializing}
5361
filter={filter}
5462
error={error}

site/src/pages/WorkspacesPage/WorkspacesPage.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Helmet } from "react-helmet-async"
44
import { pageTitle } from "utils/page"
55
import { useWorkspacesData, useWorkspaceUpdate } from "./data"
66
import { WorkspacesPageView } from "./WorkspacesPageView"
7-
import { useMe, useOrganizationId, usePermissions } from "hooks"
7+
import { useOrganizationId, usePermissions } from "hooks"
88
import {
99
useUserFilterMenu,
1010
useTemplateFilterMenu,
@@ -15,15 +15,14 @@ import { useDashboard } from "components/Dashboard/DashboardProvider"
1515
import { useFilter } from "components/Filter/filter"
1616

1717
const WorkspacesPage: FC = () => {
18-
const me = useMe()
1918
const orgId = useOrganizationId()
2019
// If we use a useSearchParams for each hook, the values will not be in sync.
2120
// So we have to use a single one, centralizing the values, and pass it to
2221
// each hook.
2322
const searchParamsResult = useSearchParams()
2423
const pagination = usePagination({ searchParamsResult })
2524
const filter = useFilter({
26-
initialValue: `owner:${me.username}`,
25+
initialValue: `owner:me`,
2726
searchParamsResult,
2827
onUpdate: () => {
2928
pagination.goToPage(1)

site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const mockMenu = {
8484

8585
const defaultFilterProps = {
8686
filter: {
87-
query: `owner:${MockUser.username}`,
87+
query: `owner:me`,
8888
update: () => action("update"),
8989
debounceUpdate: action("debounce") as any,
9090
used: false,

site/src/pages/WorkspacesPage/filter/filter.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ import {
1414
SearchFieldSkeleton,
1515
useFilter,
1616
} from "components/Filter/filter"
17+
import { workspaceFilterQuery } from "utils/filters"
18+
19+
const PRESET_FILTERS = [
20+
{ query: workspaceFilterQuery.me, name: "My workspaces" },
21+
{ query: workspaceFilterQuery.all, name: "All workspaces" },
22+
{
23+
query: workspaceFilterQuery.running,
24+
name: "Running workspaces",
25+
},
26+
{
27+
query: workspaceFilterQuery.failed,
28+
name: "Failed workspaces",
29+
},
30+
]
1731

1832
export const WorkspacesFilter = ({
1933
filter,
@@ -30,9 +44,11 @@ export const WorkspacesFilter = ({
3044
}) => {
3145
return (
3246
<Filter
47+
presets={PRESET_FILTERS}
3348
isLoading={menus.status.isInitializing}
3449
filter={filter}
3550
error={error}
51+
learnMoreLink="https://coder.com/docs/v2/latest/workspaces#workspace-filtering"
3652
options={
3753
<>
3854
{menus.user && <UserMenu {...menus.user} />}

site/src/pages/WorkspacesPage/filter/menus.ts

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ export const useUserFilterMenu = ({
2929
value,
3030
id: "owner",
3131
getSelectedOption: async () => {
32+
if (value === "me") {
33+
return {
34+
label: me.username,
35+
value: me.username,
36+
avatarUrl: me.avatar_url,
37+
}
38+
}
39+
3240
const usersRes = await getUsers({ q: value, limit: 1 })
3341
const firstUser = usersRes.users.at(0)
3442
if (firstUser && firstUser.username === value) {

0 commit comments

Comments
 (0)