Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Catalog: Bucket permissions: GraphQL #2228

Merged
merged 7 commits into from
Jul 1, 2021
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
3 changes: 0 additions & 3 deletions catalog/app/containers/Admin/Buckets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1016,10 +1016,8 @@ function CRUD({ bucketName }: CRUDProps) {
</M.Dialog>

<Table.Toolbar heading="Buckets" actions={toolbarActions} />
{/* @ts-expect-error */}
<Table.Wrapper>
<M.Table size="small">
{/* @ts-expect-error */}
<Table.Head columns={columns} ordering={ordering} withInlineActions />
<M.TableBody>
{pagination.paginated.map((i: BucketConfig) => (
Expand All @@ -1041,7 +1039,6 @@ function CRUD({ bucketName }: CRUDProps) {
padding="none"
onClick={(e) => e.stopPropagation()}
>
{/* @ts-expect-error */}
<Table.InlineActions actions={inlineActions(i)} />
</M.TableCell>
</M.TableRow>
Expand Down
143 changes: 143 additions & 0 deletions catalog/app/containers/Admin/BucketsPermissions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import * as R from 'ramda'
import * as React from 'react'
import * as M from '@material-ui/core'

import * as Model from 'model'
import StyledLink from 'utils/StyledLink'
import * as Types from 'utils/types'

const useBucketPermissionStyles = M.makeStyles((t) => ({
bucket: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
button: {
marginLeft: t.spacing(1),
},
cell: {
minWidth: t.spacing(17.5),
},
row: {
'&:last-child $cell': {
borderBottom: 0,
},
},
}))

interface BucketPermissionProps {
bucket: string
value: Model.GQLTypes.BucketPermissionLevel | null
onChange: (bucket: string, value: Model.GQLTypes.BucketPermissionLevel | null) => void
}

function BucketPermissionEdit({ bucket, value, onChange }: BucketPermissionProps) {
const classes = useBucketPermissionStyles()
const handleChange = React.useCallback(
(event) => {
const level = Types.decode(Model.NullableBucketPermissionLevelFromString)(
event.target.value,
)
onChange(bucket, level)
},
[bucket, onChange],
)
const levelStr = Model.NullableBucketPermissionLevelFromString.encode(value)
return (
<M.TableRow className={classes.row}>
<M.TableCell className={classes.cell}>
<M.Typography className={classes.bucket} variant="body1">
{bucket}
</M.Typography>
</M.TableCell>
<M.TableCell className={classes.cell}>
<M.Select native value={levelStr} onChange={handleChange}>
{Model.BucketPermissionLevelStrings.map((permission) => (
<option key={permission}>{permission}</option>
))}
</M.Select>
</M.TableCell>
</M.TableRow>
)
}

const useStyles = M.makeStyles((t) => ({
caption: {
color: t.palette.text.secondary,
},
captionWrapper: {
margin: t.spacing(0.5, 0, 0),
},
cell: {
minWidth: t.spacing(17.5),
},
permissions: {
marginTop: t.spacing(1),
},
scrollable: {
border: `1px solid ${t.palette.divider}`,
margin: t.spacing(2, 0, 0),
maxHeight: '300px',
overflow: 'auto',
},
}))

interface BucketPermissionsProps {
className: string
input: {
value: Model.GQLTypes.PermissionInput[]
onChange: (value: Model.GQLTypes.PermissionInput[]) => void
}
onAdvanced?: () => void
}

export default function BucketPermissions({
className,
input: { value, onChange },
onAdvanced,
}: BucketPermissionsProps) {
const classes = useStyles()

const handleChange = React.useCallback(
(bucket: string, level: Model.GQLTypes.BucketPermissionLevel | null) => {
const idx = R.findIndex(R.propEq('bucket', bucket), value)
onChange(R.adjust(idx, R.assoc('level', level), value))
},
[value, onChange],
)

return (
<div className={className}>
<M.Typography variant="h6">Bucket access</M.Typography>
{!!onAdvanced && (
<p className={classes.captionWrapper}>
<M.Typography className={classes.caption} variant="caption">
Manage access using per-bucket permissions or{' '}
<StyledLink onClick={onAdvanced}>set existing role via ARN</StyledLink>
</M.Typography>
</p>
)}

<M.TableContainer className={classes.scrollable}>
<M.Table size="small" className={classes.permissions}>
<M.TableHead>
<M.TableRow>
<M.TableCell className={classes.cell}>Bucket name</M.TableCell>
<M.TableCell className={classes.cell}>Permissions</M.TableCell>
</M.TableRow>
</M.TableHead>
<M.TableBody>
{value.map(({ bucket, level }) => (
<BucketPermissionEdit
key={bucket}
bucket={bucket}
value={level}
onChange={handleChange}
/>
))}
</M.TableBody>
</M.Table>
</M.TableContainer>
</div>
)
}
50 changes: 50 additions & 0 deletions catalog/app/containers/Admin/RFForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react'
import * as M from '@material-ui/core'

type FieldProps = M.TextFieldProps & {
meta: $TSFixMe
input: {
value: $TSFixMe
onChange: (value: $TSFixMe) => void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, that you are in a hurry :)
So, I'm approving now and review later, even after merging

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem with this is more fundamental -- we need a way to define form typings properly, so every field is strongly typed wrt field value, validator, initial value, errors and such (smth like this probably: https://github.com/ForbesLindesay/define-form/tree/master/packages/react-define-form), otherwise it doesnt make much sense

}
error?: string
errors: Record<string, string>
helperText?: React.ReactNode
validating?: boolean
}

export function Field({ input, meta, errors, helperText, ...rest }: FieldProps) {
const error = meta.submitFailed && (meta.error || meta.submitError)
const props = {
error: !!error,
helperText: error ? errors[error] || error : helperText,
disabled: meta.submitting || meta.submitSucceeded,
...input,
...rest,
}
return <M.TextField {...props} />
}

const useFormErrorStyles = M.makeStyles((t) => ({
root: {
marginTop: t.spacing(3),

'& a': {
textDecoration: 'underline',
},
},
}))

type FormErrorProps = M.TypographyProps & {
error?: string
errors: Record<string, string>
}

export function FormError({ error, errors, ...rest }: FormErrorProps) {
const classes = useFormErrorStyles()
return !error ? null : (
<M.Typography color="error" classes={classes} {...rest}>
{errors[error] || error}
</M.Typography>
)
}
92 changes: 92 additions & 0 deletions catalog/app/containers/Admin/RoleSelection.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
import * as Types from '../../model/graphql/types.generated'

export type RoleSelection_UnmanagedRole_Fragment = {
readonly __typename: 'UnmanagedRole'
} & Pick<Types.UnmanagedRole, 'id' | 'name' | 'arn'>

export type RoleSelection_ManagedRole_Fragment = {
readonly __typename: 'ManagedRole'
} & Pick<Types.ManagedRole, 'id' | 'name'> & {
readonly permissions: ReadonlyArray<
{ readonly __typename: 'RoleBucketPermission' } & Pick<
Types.RoleBucketPermission,
'level'
> & {
readonly bucket: { readonly __typename: 'BucketConfig' } & Pick<
Types.BucketConfig,
'name'
>
}
>
}

export type RoleSelectionFragment =
| RoleSelection_UnmanagedRole_Fragment
| RoleSelection_ManagedRole_Fragment

export const RoleSelectionFragmentDoc = ({
kind: 'Document',
definitions: [
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'RoleSelection' },
typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Role' } },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'InlineFragment',
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'UnmanagedRole' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{ kind: 'Field', name: { kind: 'Name', value: 'arn' } },
],
},
},
{
kind: 'InlineFragment',
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'ManagedRole' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'permissions' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'bucket' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
],
},
},
{ kind: 'Field', name: { kind: 'Name', value: 'level' } },
],
},
},
],
},
},
],
},
},
],
} as unknown) as DocumentNode<RoleSelectionFragment, unknown>
15 changes: 15 additions & 0 deletions catalog/app/containers/Admin/RoleSelection.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
fragment RoleSelection on Role {
... on UnmanagedRole {
id
name
arn
}
... on ManagedRole {
id
name
permissions {
bucket { name }
level
}
}
}
Loading