Skip to content
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