Skip to content

Commit

Permalink
Filter for Admin/Users and Admin/Buckets tables (#3480)
Browse files Browse the repository at this point in the history
  • Loading branch information
fiskus authored Jun 7, 2023
1 parent c8dba46 commit e734abc
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 5 deletions.
18 changes: 16 additions & 2 deletions catalog/app/containers/Admin/Buckets/Buckets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useTracker } from 'utils/tracking'
import * as Types from 'utils/types'
import * as validators from 'utils/validators'

import Filter from '../Filter'
import * as Form from '../Form'
import * as Table from '../Table'

Expand Down Expand Up @@ -1272,7 +1273,17 @@ interface CRUDProps {

function CRUD({ bucketName }: CRUDProps) {
const { bucketConfigs: rows } = GQL.useQueryS(BUCKET_CONFIGS_QUERY)
const ordering = Table.useOrdering({ rows, column: columns[0] })
const [filter, setFilter] = React.useState('')
const filtered = React.useMemo(
() =>
filter
? rows.filter(({ name, title }) =>
(name + title).toLowerCase().includes(filter.toLowerCase()),
)
: rows,
[filter, rows],
)
const ordering = Table.useOrdering({ rows: filtered, column: columns[0] })
const pagination = Pagination.use(ordering.ordered, {
// @ts-expect-error
getItemId: R.prop('name'),
Expand Down Expand Up @@ -1335,7 +1346,10 @@ function CRUD({ bucketName }: CRUDProps) {
{editingBucket && <Edit bucket={editingBucket} close={onBucketClose} />}
</M.Dialog>

<Table.Toolbar heading="Buckets" actions={toolbarActions} />
<Table.Toolbar heading="Buckets" actions={toolbarActions}>
{/* @ts-expect-error */}
<Filter value={filter} onChange={setFilter} />
</Table.Toolbar>
<Table.Wrapper>
<M.Table size="small">
<Table.Head columns={columns} ordering={ordering} withInlineActions />
Expand Down
126 changes: 126 additions & 0 deletions catalog/app/containers/Admin/Filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import cx from 'classnames'
import * as React from 'react'
import * as M from '@material-ui/core'

const ANIMATION_DURATION = 150

function SearchIcon() {
return (
<M.InputAdornment position="start">
<M.Icon>search</M.Icon>
</M.InputAdornment>
)
}

interface ClearButtonProps {
onClick: () => void
}

function ClearButton({ onClick }: ClearButtonProps) {
return (
<M.InputAdornment position="end" onClick={onClick}>
<M.IconButton size="small">
<M.Icon fontSize="small">clear</M.Icon>
</M.IconButton>
</M.InputAdornment>
)
}

const useFilterStyles = M.makeStyles({
root: {
animation: `$expand ${ANIMATION_DURATION}ms ease-out`,
width: '40vw',
},
collapsing: {
animation: `$collapse ${ANIMATION_DURATION}ms ease-in`,
},
'@keyframes collapse': {
// Scaling down doesn't look good
'0%': {
transform: 'translateX(0)',
},
'100%': {
transform: 'translateX(2vw)',
},
},
'@keyframes expand': {
'0%': {
transform: 'scaleX(0.8)',
},
'100%': {
transform: 'scaleX(1)',
},
},
})

interface FilterContainerProps {
value: string
onChange: (v: string) => void
}

interface FilterProps extends FilterContainerProps {
onClose: () => void
}

function Filter({ onChange, onClose, value }: FilterProps) {
const classes = useFilterStyles()
const [collapsing, setCollapsing] = React.useState(false)
const collapse = React.useCallback(() => {
setCollapsing(true)
// "Close" before animation ends
setTimeout(onClose, ANIMATION_DURATION * 0.8)
}, [onClose])
const handleBlur = React.useCallback(() => {
if (value) return
collapse()
}, [collapse, value])
const handleChange = React.useCallback(
(event) => onChange(event.target.value),
[onChange],
)
const handleClear = React.useCallback(() => {
onChange('')
collapse()
}, [collapse, onChange])

const InputProps = React.useMemo(
() => ({
// Underline has its own animation and doesn't play well with collapse animation
disableUnderline: collapsing,
startAdornment: <SearchIcon />,
endAdornment: value && <ClearButton onClick={handleClear} />,
}),
[collapsing, value, handleClear],
)

return (
<M.ClickAwayListener onClickAway={handleBlur}>
<M.TextField
InputProps={InputProps}
autoFocus
className={cx(classes.root, { [classes.collapsing]: collapsing })}
onBlur={handleBlur}
onChange={handleChange}
placeholder="Filter"
size="small"
value={value}
/>
</M.ClickAwayListener>
)
}

export default function FilterContainer({ onChange, value }: FilterContainerProps) {
const [expanded, setExpanded] = React.useState(false)
const expand = React.useCallback(() => setExpanded(true), [])
const collapse = React.useCallback(() => setExpanded(false), [])

if (!expanded) {
return (
<M.IconButton onClick={expand}>
<M.Icon>search</M.Icon>
</M.IconButton>
)
}

return <Filter onChange={onChange} onClose={collapse} value={value} />
}
9 changes: 8 additions & 1 deletion catalog/app/containers/Admin/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ const useToolbarStyles = M.makeStyles((t) => ({
},
}))

export function Toolbar({ heading, selected = 0, actions = [], selectedActions = [] }) {
export function Toolbar({
heading,
selected = 0,
actions = [],
selectedActions = [],
children = null,
}) {
const classes = useToolbarStyles()
return (
<M.Toolbar className={cx(classes.root, { [classes.highlight]: selected > 0 })}>
Expand All @@ -89,6 +95,7 @@ export function Toolbar({ heading, selected = 0, actions = [], selectedActions =
)}
</div>
<div className={classes.spacer} />
{children}
<div className={classes.actions}>
{(selected > 0 ? selectedActions : actions).map(renderAction)}
</div>
Expand Down
15 changes: 13 additions & 2 deletions catalog/app/containers/Admin/Users/Users.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as Cache from 'utils/ResourceCache'
import * as Format from 'utils/format'
import * as validators from 'utils/validators'

import Filter from '../Filter'
import * as Form from '../Form'
import * as Table from '../Table'
import * as data from '../data'
Expand Down Expand Up @@ -676,7 +677,15 @@ export default function Users({ users }) {
[roles, openDialog, setIsActive, setRole],
)

const ordering = Table.useOrdering({ rows, column: columns[0] })
const [filter, setFilter] = React.useState('')
const filtered = React.useMemo(
() =>
filter
? rows.filter(({ email, username }) => (email + username).includes(filter))
: rows,
[filter, rows],
)
const ordering = Table.useOrdering({ rows: filtered, column: columns[0] })
const pagination = Pagination.use(ordering.ordered, {
getItemId: R.prop('username'),
})
Expand Down Expand Up @@ -712,7 +721,9 @@ export default function Users({ users }) {
<React.Suspense fallback={<UsersSkeleton />}>
<M.Paper>
{dialogs.render({ maxWidth: 'xs', fullWidth: true })}
<Table.Toolbar heading="Users" actions={toolbarActions} />
<Table.Toolbar heading="Users" actions={toolbarActions}>
<Filter value={filter} onChange={setFilter} />
</Table.Toolbar>
<Table.Wrapper>
<M.Table size="small">
<Table.Head columns={columns} ordering={ordering} withInlineActions />
Expand Down
8 changes: 8 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ Entries inside each section should be ordered by type:
## Catalog, Lambdas
!-->
# unreleased - YYYY-MM-DD
## Python API

## CLI

## Catalog, Lambdas
* [Added] Add filter for users and buckets tables in Admin dashboards ([#3480](https://github.com/quiltdata/quilt/pull/3480))

# 5.3.1 - 2023-05-02
## Python API
* [Fixed] `Package.verify()` now raises exception if unsupported hash type is encountered ([#3401](https://github.com/quiltdata/quilt/pull/3401))
Expand Down

0 comments on commit e734abc

Please sign in to comment.