mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: Extract workspace filter into a separate component (#2601)
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { workspaceFilterQuery } from "../../util/workspace"
|
||||
import { SearchBarWithFilter, SearchBarWithFilterProps } from "./SearchBarWithFilter"
|
||||
|
||||
export default {
|
||||
title: "components/SearchBarWithFilter",
|
||||
component: SearchBarWithFilter,
|
||||
argTypes: {
|
||||
filter: {
|
||||
defaultValue: workspaceFilterQuery.me,
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof SearchBarWithFilter>
|
||||
|
||||
const Template: Story<SearchBarWithFilterProps> = (args) => <SearchBarWithFilter {...args} />
|
||||
|
||||
export const WithoutPresetFilters = Template.bind({})
|
||||
|
||||
export const WithPresetFilters = Template.bind({})
|
||||
WithPresetFilters.args = {
|
||||
presetFilters: [
|
||||
{ query: workspaceFilterQuery.me, name: "Your workspaces" },
|
||||
{ query: "random query", name: "Random query" },
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Fade from "@material-ui/core/Fade"
|
||||
import InputAdornment from "@material-ui/core/InputAdornment"
|
||||
import Menu from "@material-ui/core/Menu"
|
||||
import MenuItem from "@material-ui/core/MenuItem"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import SearchIcon from "@material-ui/icons/Search"
|
||||
import { FormikErrors, useFormik } from "formik"
|
||||
import { useState } from "react"
|
||||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
|
||||
export const Language = {
|
||||
filterName: "Filters",
|
||||
}
|
||||
|
||||
export interface SearchBarWithFilterProps {
|
||||
filter?: string
|
||||
onFilter: (query: string) => void
|
||||
presetFilters?: PresetFilter[]
|
||||
}
|
||||
|
||||
export interface PresetFilter {
|
||||
name: string
|
||||
query: string
|
||||
}
|
||||
|
||||
interface FilterFormValues {
|
||||
query: string
|
||||
}
|
||||
|
||||
export type FilterFormErrors = FormikErrors<FilterFormValues>
|
||||
|
||||
export const SearchBarWithFilter: React.FC<SearchBarWithFilterProps> = ({ filter, onFilter, presetFilters }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
const form = useFormik<FilterFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
query: filter ?? "",
|
||||
},
|
||||
onSubmit: ({ query }) => {
|
||||
onFilter(query)
|
||||
},
|
||||
})
|
||||
|
||||
const getFieldHelpers = getFormHelpers<FilterFormValues>(form)
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const setPresetFilter = (query: string) => () => {
|
||||
void form.setFieldValue("query", query)
|
||||
void form.submitForm()
|
||||
handleClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={0} className={styles.filterContainer}>
|
||||
{presetFilters && presetFilters.length > 0 && (
|
||||
<Button aria-controls="filter-menu" aria-haspopup="true" onClick={handleClick} className={styles.buttonRoot}>
|
||||
{Language.filterName} {anchorEl ? <CloseDropdown /> : <OpenDropdown />}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.handleSubmit} className={styles.filterForm}>
|
||||
<TextField
|
||||
{...getFieldHelpers("query")}
|
||||
className={styles.textFieldRoot}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon fontSize="small" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
|
||||
{presetFilters && presetFilters.length > 0 && (
|
||||
<Menu
|
||||
id="filter-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
TransitionComponent={Fade}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
{presetFilters.map((presetFilter) => (
|
||||
<MenuItem key={presetFilter.name} onClick={setPresetFilter(presetFilter.query)}>
|
||||
{presetFilter.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
filterContainer: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
filterForm: {
|
||||
width: "100%",
|
||||
},
|
||||
buttonRoot: {
|
||||
border: "none",
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
|
||||
},
|
||||
textFieldRoot: {
|
||||
margin: "0px",
|
||||
"& fieldset": {
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -1,32 +1,25 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Fade from "@material-ui/core/Fade"
|
||||
import InputAdornment from "@material-ui/core/InputAdornment"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import Menu from "@material-ui/core/Menu"
|
||||
import MenuItem from "@material-ui/core/MenuItem"
|
||||
import { fade, makeStyles, Theme } from "@material-ui/core/styles"
|
||||
import Table from "@material-ui/core/Table"
|
||||
import TableBody from "@material-ui/core/TableBody"
|
||||
import TableCell from "@material-ui/core/TableCell"
|
||||
import TableHead from "@material-ui/core/TableHead"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
|
||||
import RefreshIcon from "@material-ui/icons/Refresh"
|
||||
import SearchIcon from "@material-ui/icons/Search"
|
||||
import useTheme from "@material-ui/styles/useTheme"
|
||||
import { useActor } from "@xstate/react"
|
||||
import dayjs from "dayjs"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import { FormikErrors, useFormik } from "formik"
|
||||
import { FC, useState } from "react"
|
||||
import { FC } from "react"
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom"
|
||||
import { AvatarData } from "../../components/AvatarData/AvatarData"
|
||||
import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/DropdownArrows"
|
||||
import { EmptyState } from "../../components/EmptyState/EmptyState"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
|
||||
import { SearchBarWithFilter } from "../../components/SearchBarWithFilter/SearchBarWithFilter"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { TableCellLink } from "../../components/TableCellLink/TableCellLink"
|
||||
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
||||
@@ -38,7 +31,6 @@ import {
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "../../components/Tooltips/HelpTooltip/HelpTooltip"
|
||||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { getDisplayStatus, workspaceFilterQuery } from "../../util/workspace"
|
||||
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
||||
|
||||
@@ -49,7 +41,6 @@ export const Language = {
|
||||
emptyCreateWorkspaceMessage: "Create your first workspace",
|
||||
emptyCreateWorkspaceDescription: "Start editing your source code and building your software",
|
||||
emptyResultsMessage: "No results matched your search",
|
||||
filterName: "Filters",
|
||||
yourWorkspacesButton: "Your workspaces",
|
||||
allWorkspacesButton: "All workspaces",
|
||||
workspaceTooltipTitle: "What is a workspace?",
|
||||
@@ -154,12 +145,6 @@ const WorkspaceRow: React.FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({ wor
|
||||
)
|
||||
}
|
||||
|
||||
interface FilterFormValues {
|
||||
query: string
|
||||
}
|
||||
|
||||
export type FilterFormErrors = FormikErrors<FilterFormValues>
|
||||
|
||||
export interface WorkspacesPageViewProps {
|
||||
loading?: boolean
|
||||
workspaceRefs?: WorkspaceItemMachineRef[]
|
||||
@@ -168,41 +153,10 @@ export interface WorkspacesPageViewProps {
|
||||
}
|
||||
|
||||
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, workspaceRefs, filter, onFilter }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
const form = useFormik<FilterFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
query: filter ?? "",
|
||||
},
|
||||
onSubmit: ({ query }) => {
|
||||
onFilter(query)
|
||||
},
|
||||
})
|
||||
|
||||
const getFieldHelpers = getFormHelpers<FilterFormValues>(form)
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const setYourWorkspaces = () => {
|
||||
void form.setFieldValue("query", "owner:me")
|
||||
void form.submitForm()
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const setAllWorkspaces = () => {
|
||||
void form.setFieldValue("query", "")
|
||||
void form.submitForm()
|
||||
handleClose()
|
||||
}
|
||||
const presetFilters = [
|
||||
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
|
||||
{ query: workspaceFilterQuery.all, name: Language.allWorkspacesButton },
|
||||
]
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
@@ -223,48 +177,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
|
||||
</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
|
||||
<Stack direction="row" spacing={0} className={styles.filterContainer}>
|
||||
<Button aria-controls="filter-menu" aria-haspopup="true" onClick={handleClick} className={styles.buttonRoot}>
|
||||
{Language.filterName} {anchorEl ? <CloseDropdown /> : <OpenDropdown />}
|
||||
</Button>
|
||||
|
||||
<form onSubmit={form.handleSubmit} className={styles.filterForm}>
|
||||
<TextField
|
||||
{...getFieldHelpers("query")}
|
||||
className={styles.textFieldRoot}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon fontSize="small" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Menu
|
||||
id="filter-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
TransitionComponent={Fade}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={setYourWorkspaces}>{Language.yourWorkspacesButton}</MenuItem>
|
||||
<MenuItem onClick={setAllWorkspaces}>{Language.allWorkspacesButton}</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
<SearchBarWithFilter filter={filter} onFilter={onFilter} presetFilters={presetFilters} />
|
||||
|
||||
<Table>
|
||||
<TableHead>
|
||||
@@ -328,25 +241,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
lineHeight: `${theme.spacing(3)}px`,
|
||||
},
|
||||
},
|
||||
filterContainer: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
filterForm: {
|
||||
width: "100%",
|
||||
},
|
||||
buttonRoot: {
|
||||
border: "none",
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
|
||||
},
|
||||
textFieldRoot: {
|
||||
margin: "0px",
|
||||
"& fieldset": {
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
clickableTableRow: {
|
||||
"&:hover td": {
|
||||
backgroundColor: fade(theme.palette.primary.light, 0.1),
|
||||
|
||||
Reference in New Issue
Block a user