From aa875ae7ece6b26ff6511304bb90c3188af732f8 Mon Sep 17 00:00:00 2001 From: Maxim Chervonny Date: Fri, 11 Aug 2023 18:03:47 +0300 Subject: [PATCH] Add filter to Policies and Roles tables, refactor filter --- .../app/containers/Admin/Buckets/Buckets.tsx | 31 ++- .../RolesAndPolicies/AssociatedRoles.tsx | 27 ++- .../RolesAndPolicies/AttachedPolicies.tsx | 71 ++++-- .../RolesAndPolicies/BucketsPermissions.tsx | 68 ++++-- .../Admin/RolesAndPolicies/Filter.tsx | 26 +++ .../Admin/RolesAndPolicies/Policies.tsx | 15 +- .../Admin/RolesAndPolicies/Roles.tsx | 15 +- .../containers/Admin/{ => Table}/Filter.tsx | 0 .../Admin/{Table.js => Table/Table.tsx} | 215 ++++++++++-------- catalog/app/containers/Admin/Table/index.js | 3 + catalog/app/containers/Admin/Table/untyped.js | 72 ++++++ catalog/app/containers/Admin/Users/Users.js | 23 +- docs/CHANGELOG.md | 1 + 13 files changed, 379 insertions(+), 188 deletions(-) create mode 100644 catalog/app/containers/Admin/RolesAndPolicies/Filter.tsx rename catalog/app/containers/Admin/{ => Table}/Filter.tsx (100%) rename catalog/app/containers/Admin/{Table.js => Table/Table.tsx} (54%) create mode 100644 catalog/app/containers/Admin/Table/index.js create mode 100644 catalog/app/containers/Admin/Table/untyped.js diff --git a/catalog/app/containers/Admin/Buckets/Buckets.tsx b/catalog/app/containers/Admin/Buckets/Buckets.tsx index 0bb5b6b8236..db04103b3b6 100644 --- a/catalog/app/containers/Admin/Buckets/Buckets.tsx +++ b/catalog/app/containers/Admin/Buckets/Buckets.tsx @@ -29,7 +29,6 @@ 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' @@ -1266,23 +1265,25 @@ const columns = [ }, ] +type BucketRow = Pick< + Model.GQLTypes.BucketConfig, + 'name' | 'title' | 'description' | 'lastIndexed' | 'iconUrl' +> + interface CRUDProps { bucketName?: string } function CRUD({ bucketName }: CRUDProps) { const { bucketConfigs: rows } = GQL.useQueryS(BUCKET_CONFIGS_QUERY) - 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 filtering = Table.useFiltering({ + rows, + filterBy: ({ name, title }) => name + title, + }) + const ordering = Table.useOrdering({ + rows: filtering.filtered, + column: columns[0], + }) const pagination = Pagination.use(ordering.ordered, { // @ts-expect-error getItemId: R.prop('name'), @@ -1293,6 +1294,7 @@ function CRUD({ bucketName }: CRUDProps) { const history = RRDom.useHistory() const toolbarActions = [ + , { title: 'Add bucket', icon: add, @@ -1345,10 +1347,7 @@ function CRUD({ bucketName }: CRUDProps) { {editingBucket && } - - {/* @ts-expect-error */} - - + diff --git a/catalog/app/containers/Admin/RolesAndPolicies/AssociatedRoles.tsx b/catalog/app/containers/Admin/RolesAndPolicies/AssociatedRoles.tsx index 3b0c8c16853..5d0eb3cc5fa 100644 --- a/catalog/app/containers/Admin/RolesAndPolicies/AssociatedRoles.tsx +++ b/catalog/app/containers/Admin/RolesAndPolicies/AssociatedRoles.tsx @@ -1,9 +1,12 @@ import * as React from 'react' +import * as R from 'ramda' import * as RF from 'react-final-form' import * as M from '@material-ui/core' import * as GQL from 'utils/GraphQL' +import * as Table from '../Table' +import Filter from './Filter' import { MAX_POLICIES_PER_ROLE } from './shared' import ROLES_QUERY from './gql/Roles.generated' @@ -45,12 +48,26 @@ function RoleSelectionDialog({ [setSelected], ) + const filtering = Table.useFiltering({ + rows: roles, + filterBy: ({ name }: ManagedRole) => name, + }) + const ordered = React.useMemo( + () => R.sortBy(({ name }: ManagedRole) => name, filtering.filtered), + [filtering.filtered], + ) + return ( Attach policy to roles + {roles.length && ( + + + + )} - {roles.length ? ( - roles.map((role) => ( + {ordered.length ? ( + ordered.map((role) => ( )) ) : ( - No more roles to attach this policy to + + {filtering.value + ? 'No roles found, try resetting filter' + : 'No more roles to attach this policy to'} + )} diff --git a/catalog/app/containers/Admin/RolesAndPolicies/AttachedPolicies.tsx b/catalog/app/containers/Admin/RolesAndPolicies/AttachedPolicies.tsx index d31f1ed8e56..b00f4f477e2 100644 --- a/catalog/app/containers/Admin/RolesAndPolicies/AttachedPolicies.tsx +++ b/catalog/app/containers/Admin/RolesAndPolicies/AttachedPolicies.tsx @@ -1,10 +1,13 @@ import * as React from 'react' +import * as R from 'ramda' import * as RF from 'react-final-form' import * as M from '@material-ui/core' import * as GQL from 'utils/GraphQL' import StyledLink from 'utils/StyledLink' +import * as Table from '../Table' +import Filter from './Filter' import { MAX_POLICIES_PER_ROLE } from './shared' import POLICIES_QUERY from './gql/Policies.generated' @@ -38,34 +41,52 @@ function PolicySelectionDialog({ [setSelected, onClose], ) + const filtering = Table.useFiltering({ + rows: policies, + filterBy: ({ title }: Policy) => title, + }) + const ordered = React.useMemo( + () => R.sortBy(({ title }: Policy) => title, filtering.filtered), + [filtering.filtered], + ) + return ( Attach a policy - - {policies.length ? ( - policies.map((policy) => ( - select(policy)}> - - {policy.title} - - {' '} - ( - {policy.managed ? ( - <>{policy.permissions.length} buckets - ) : ( - <>unmanaged - )} - ) - - - - )) - ) : ( - - No more policies to attach - - )} - + {policies.length && ( + + + + )} + + + {ordered.length ? ( + ordered.map((policy) => ( + select(policy)}> + + {policy.title} + + {' '} + ( + {policy.managed ? ( + <>{policy.permissions.length} buckets + ) : ( + <>unmanaged + )} + ) + + + + )) + ) : ( + + {filtering.value + ? 'No policies found, try resetting filter' + : 'No more policies to attach'} + + )} + + Cancel diff --git a/catalog/app/containers/Admin/RolesAndPolicies/BucketsPermissions.tsx b/catalog/app/containers/Admin/RolesAndPolicies/BucketsPermissions.tsx index 3b7631b5289..a61d326a2ac 100644 --- a/catalog/app/containers/Admin/RolesAndPolicies/BucketsPermissions.tsx +++ b/catalog/app/containers/Admin/RolesAndPolicies/BucketsPermissions.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import * as R from 'ramda' import * as RF from 'react-final-form' import * as M from '@material-ui/core' @@ -7,6 +8,9 @@ import * as Model from 'model' import * as GQL from 'utils/GraphQL' import StyledLink from 'utils/StyledLink' +import * as Table from '../Table' +import Filter from './Filter' + import BUCKETS_QUERY from './gql/Buckets.generated' import { BucketPermissionSelectionFragment as BucketPermission } from './gql/BucketPermissionSelection.generated' @@ -39,33 +43,51 @@ function BucketAddDialog({ open, onClose, buckets, addBucket }: BucketAddDialogP [onClose, select], ) + const filtering = Table.useFiltering({ + rows: buckets, + filterBy: ({ name, title }: Bucket) => name + title, + }) + const ordered = React.useMemo( + () => R.sortBy(({ name }: Bucket) => name, filtering.filtered), + [filtering.filtered], + ) + return ( Add a bucket - {buckets.length ? ( - - {buckets.map((bucket) => ( - handleAdd(bucket)}> - - - - - s3://{bucket.name}{' '} - - {bucket.title} - - - - ))} - - ) : ( - - No more buckets to add - + {buckets.length && ( + + + )} + + {ordered.length ? ( + + {ordered.map((bucket) => ( + handleAdd(bucket)}> + + + + + s3://{bucket.name}{' '} + + {bucket.title} + + + + ))} + + ) : ( + + {filtering.value + ? 'No buckets found, try resetting filter' + : 'No more buckets to add'} + + )} + Cancel diff --git a/catalog/app/containers/Admin/RolesAndPolicies/Filter.tsx b/catalog/app/containers/Admin/RolesAndPolicies/Filter.tsx new file mode 100644 index 00000000000..193a83cfb02 --- /dev/null +++ b/catalog/app/containers/Admin/RolesAndPolicies/Filter.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' +import * as M from '@material-ui/core' + +interface FilterProps { + value: string + onChange: (v: string) => void +} + +export default function Filter({ value, onChange }: FilterProps) { + return ( + onChange('')}> + clear + + ) + } + fullWidth + onChange={(event) => onChange(event.target.value)} + placeholder="Filter" + startAdornment={search} + value={value} + /> + ) +} diff --git a/catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx b/catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx index ba8a6c73801..56458d08b12 100644 --- a/catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx +++ b/catalog/app/containers/Admin/RolesAndPolicies/Policies.tsx @@ -649,10 +649,18 @@ interface DialogsOpenProps { export default function Policies() { const { policies: rows } = GQL.useQueryS(POLICIES_QUERY) - const ordering = Table.useOrdering({ rows, column: columns[0] }) + const filtering = Table.useFiltering({ + rows, + filterBy: ({ title }) => title, + }) + const ordering = Table.useOrdering({ + rows: filtering.filtered, + column: columns[0], + }) const dialogs = Dialogs.use() const toolbarActions = [ + , { title: 'Create', icon: add, @@ -664,11 +672,11 @@ export default function Policies() { const inlineActions = (policy: Policy) => [ policy.arn - ? { + ? ({ title: 'Open AWS Console', icon: launch, href: getArnLink(policy.arn), - } + } as Table.Action) : null, { title: 'Edit', @@ -712,7 +720,6 @@ export default function Policies() { ))} - {/* @ts-expect-error */} diff --git a/catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx b/catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx index 9ac39b883ef..6ef332f4924 100644 --- a/catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx +++ b/catalog/app/containers/Admin/RolesAndPolicies/Roles.tsx @@ -694,10 +694,18 @@ export default function Roles() { const rows = data.roles const defaultRoleId = data.defaultRole?.id - const ordering = Table.useOrdering({ rows, column: columns[0] }) + const filtering = Table.useFiltering({ + rows, + filterBy: ({ name }) => name, + }) + const ordering = Table.useOrdering({ + rows: filtering.filtered, + column: columns[0], + }) const dialogs = Dialogs.use() const toolbarActions = [ + , { title: 'Create', icon: add, @@ -709,11 +717,11 @@ export default function Roles() { const inlineActions = (role: Role) => [ role.arn - ? { + ? ({ title: 'Open AWS Console', icon: launch, href: getArnLink(role.arn), - } + } as Table.Action) : null, { title: 'Edit', @@ -760,7 +768,6 @@ export default function Roles() { ))} - {/* @ts-expect-error */} diff --git a/catalog/app/containers/Admin/Filter.tsx b/catalog/app/containers/Admin/Table/Filter.tsx similarity index 100% rename from catalog/app/containers/Admin/Filter.tsx rename to catalog/app/containers/Admin/Table/Filter.tsx diff --git a/catalog/app/containers/Admin/Table.js b/catalog/app/containers/Admin/Table/Table.tsx similarity index 54% rename from catalog/app/containers/Admin/Table.js rename to catalog/app/containers/Admin/Table/Table.tsx index db4b529c39f..f9cae309a82 100644 --- a/catalog/app/containers/Admin/Table.js +++ b/catalog/app/containers/Admin/Table/Table.tsx @@ -6,15 +6,58 @@ import { lighten } from '@material-ui/core/styles/colorManipulator' import useMemoEq from 'utils/useMemoEq' -const changeDirection = (d) => (d === 'asc' ? 'desc' : 'asc') +interface UseFilteringProps { + rows: readonly Row[] + filterBy: (r: Row) => string +} + +export function useFiltering({ rows, filterBy }: UseFilteringProps) { + const [value, onChange] = React.useState('') + const filtered = React.useMemo( + () => + value + ? R.filter( + (row: Row) => filterBy(row).toLowerCase().includes(value.toLowerCase()), + rows, + ) + : rows, + [filterBy, value, rows], + ) + return { value, onChange, filtered } +} -export function useOrdering({ rows, ...opts }) { +type Direction = 'asc' | 'desc' + +const changeDirection = (d: Direction) => (d === 'asc' ? 'desc' : 'asc') + +export type Column = { + align?: string + getValue: $TSFixMe + hint?: M.TooltipProps['title'] + id: string + label: React.ReactNode + // props?: M.TableCellProps + sortBy?: (r: Row) => string + sortable?: boolean +} + +interface UseOrderingProps { + rows: readonly Row[] + direction?: Direction + column: Column +} + +export function useOrdering({ rows, ...opts }: UseOrderingProps) { const [column, setColumn] = React.useState(opts.column) const [direction, setDirection] = React.useState(opts.direction || 'asc') const sortBy = column.sortBy || column.getValue const sort = React.useMemo( - () => R.pipe(R.sortBy(sortBy), direction === 'asc' ? R.identity : R.reverse), + () => + R.pipe( + R.sortBy(sortBy), + direction === 'asc' ? R.identity : R.reverse, + ), [sortBy, direction], ) @@ -35,20 +78,45 @@ export function useOrdering({ rows, ...opts }) { return { column, direction, change, ordered } } -export const renderAction = (a) => - !a ? null : ( +export type Action = + | React.ReactElement + | { + href: string + icon: React.ReactNode + title: string + fn?: () => void + } + | { + fn: () => void + icon: React.ReactNode + title: string + href?: undefined + } + | null + +export const renderAction = (a: Action) => { + if (!a) return null + if (React.isValidElement(a)) return a + return ( - - {a.icon} - + {a.href ? ( + + {a.icon} + + ) : ( + + {a.icon} + + )} ) +} const useToolbarStyles = M.makeStyles((t) => ({ root: { @@ -68,20 +136,29 @@ const useToolbarStyles = M.makeStyles((t) => ({ flex: '1 1 100%', }, actions: { + alignItems: 'center', color: t.palette.text.secondary, + display: 'flex', }, title: { flex: '0 0 auto', }, })) +interface ToolbarProps { + heading: React.ReactNode + selected?: number + actions?: Action[] + selectedActions?: Action[] + filtering?: { value: string; onChange: (f: string) => void } +} + export function Toolbar({ heading, selected = 0, actions = [], selectedActions = [], - children = null, -}) { +}: ToolbarProps) { const classes = useToolbarStyles() return ( 0 })}> @@ -95,7 +172,6 @@ export function Toolbar({ )}
- {children}
{(selected > 0 ? selectedActions : actions).map(renderAction)}
@@ -103,65 +179,6 @@ export function Toolbar({ ) } -export function Head({ - columns, - selection: sel = undefined, - ordering: ord, - withInlineActions = false, -}) { - return ( - - - {!!sel && ( - - 0 && sel.selected.size < sel.all.size} - checked={sel.selected.equals(sel.all)} - /> - - )} - {columns.map((col) => ( - - {col.sortable === false ? ( - col.label - ) : ( - - ord.change(col)} - > - {col.label} - - - )} - - ))} - {withInlineActions && Actions} - - - ) -} - -const useWrapperStyles = M.makeStyles({ - root: { - overflowX: 'auto', - }, -}) - -export function Wrapper({ className = undefined, ...props }) { - const classes = useWrapperStyles() - return
-} - const useInlineActionsStyles = M.makeStyles((t) => ({ root: { opacity: 0.3, @@ -176,7 +193,16 @@ const useInlineActionsStyles = M.makeStyles((t) => ({ }, })) -export function InlineActions({ actions = [], children = undefined, ...props }) { +interface InlineActionsProps extends React.HTMLAttributes { + actions?: Action[] + children?: React.ReactNode +} + +export function InlineActions({ + actions = [], + children = undefined, + ...props +}: InlineActionsProps) { const classes = useInlineActionsStyles() return (
@@ -186,6 +212,17 @@ export function InlineActions({ actions = [], children = undefined, ...props }) ) } +const useWrapperStyles = M.makeStyles({ + root: { + overflowX: 'auto', + }, +}) + +export function Wrapper({ className = undefined, ...props }) { + const classes = useWrapperStyles() + return
+} + const useProgressStyles = M.makeStyles((t) => ({ root: { marginBottom: t.spacing(2), @@ -193,29 +230,7 @@ const useProgressStyles = M.makeStyles((t) => ({ }, })) -export function Progress(props) { +export function Progress(props: M.CircularProgressProps) { const classes = useProgressStyles() return } - -const usePaginationStyles = M.makeStyles((t) => ({ - toolbar: { - paddingRight: [t.spacing(1), '!important'], - }, -})) - -export function Pagination({ pagination, ...rest }) { - const classes = usePaginationStyles() - return ( - pagination.goToPage(page + 1)} - onChangeRowsPerPage={(e) => pagination.setPerPage(e.target.value)} - {...rest} - /> - ) -} diff --git a/catalog/app/containers/Admin/Table/index.js b/catalog/app/containers/Admin/Table/index.js new file mode 100644 index 00000000000..bc4430fd71b --- /dev/null +++ b/catalog/app/containers/Admin/Table/index.js @@ -0,0 +1,3 @@ +export * from './untyped' +export * from './Table' +export { default as Filter } from './Filter' diff --git a/catalog/app/containers/Admin/Table/untyped.js b/catalog/app/containers/Admin/Table/untyped.js new file mode 100644 index 00000000000..55d9ab386ea --- /dev/null +++ b/catalog/app/containers/Admin/Table/untyped.js @@ -0,0 +1,72 @@ +import * as React from 'react' +import * as M from '@material-ui/core' + +export function Head({ + columns, + selection: sel = undefined, + ordering: ord, + withInlineActions = false, +}) { + return ( + + + {!!sel && ( + + 0 && sel.selected.size < sel.all.size} + checked={sel.selected.equals(sel.all)} + /> + + )} + {columns.map((col) => ( + + {col.sortable === false ? ( + col.label + ) : ( + + ord.sort(col)} + > + {col.label} + + + )} + + ))} + {withInlineActions && Actions} + + + ) +} + +const usePaginationStyles = M.makeStyles((t) => ({ + toolbar: { + paddingRight: [t.spacing(1), '!important'], + }, +})) + +export function Pagination({ pagination, ...rest }) { + const classes = usePaginationStyles() + return ( + pagination.goToPage(page + 1)} + onChangeRowsPerPage={(e) => pagination.setPerPage(e.target.value)} + {...rest} + /> + ) +} diff --git a/catalog/app/containers/Admin/Users/Users.js b/catalog/app/containers/Admin/Users/Users.js index 6c4d09aed25..c68155d12dd 100644 --- a/catalog/app/containers/Admin/Users/Users.js +++ b/catalog/app/containers/Admin/Users/Users.js @@ -14,7 +14,6 @@ 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' @@ -677,20 +676,20 @@ export default function Users({ users }) { [roles, openDialog, setIsActive, setRole], ) - 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 filtering = Table.useFiltering({ + rows, + filterBy: ({ email, username }) => email + username, + }) + const ordering = Table.useOrdering({ + rows: filtering.filtered, + column: columns[0], + }) const pagination = Pagination.use(ordering.ordered, { getItemId: R.prop('username'), }) const toolbarActions = [ + , { title: 'Invite', icon: add, @@ -721,9 +720,7 @@ export default function Users({ users }) { }> {dialogs.render({ maxWidth: 'xs', fullWidth: true })} - - - + diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 713670d6286..006b2a59b34 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -28,6 +28,7 @@ Entries inside each section should be ordered by type: * [Added] Add links to documentation and re-use code samples ([#3496](https://github.com/quiltdata/quilt/pull/3496)) * [Added] Show S3 Object tags ([#3515](https://github.com/quiltdata/quilt/pull/3515)) * [Added] Indexer lambda now indexes S3 Object tags ([#3691](https://github.com/quiltdata/quilt/pull/3691)) +* [Added] Add filters to Roles and Permissions in Admin dashboards ([#3690](https://github.com/quiltdata/quilt/pull/3690)) * [Changed] Enable user selection in perspective grids ([#3453](https://github.com/quiltdata/quilt/pull/3453)) * [Changed] Hide columns without values in files listings ([#3512](https://github.com/quiltdata/quilt/pull/3512)) * [Changed] Enable `allow-same-origin` for iframes in browsable buckets ([#3516](https://github.com/quiltdata/quilt/pull/3516))