Skip to content

Commit

Permalink
View and edit SSO config (#4072)
Browse files Browse the repository at this point in the history
  • Loading branch information
fiskus authored Aug 12, 2024
1 parent 1953dcf commit 66f7d0a
Show file tree
Hide file tree
Showing 12 changed files with 484 additions and 13 deletions.
12 changes: 9 additions & 3 deletions catalog/app/components/Lock/Lock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ import { fade } from '@material-ui/core/styles'

const useStyles = M.makeStyles((t) => ({
root: {
alignItems: 'center',
background: fade(t.palette.background.paper, 0.5),
bottom: 0,
cursor: 'not-allowed',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
left: 0,
position: 'absolute',
right: 0,
top: 0,
zIndex: 1,
zIndex: 3, // above Select, Checkbox and sticky table header
},
}))

interface LockProps {
className?: string
children?: React.ReactNode
}

export default function Lock({ className }: LockProps) {
export default function Lock({ children, className }: LockProps) {
const classes = useStyles()
return <div className={cx(classes.root, className)} />
return <div className={cx(classes.root, className)}>{children}</div>
}
1 change: 1 addition & 0 deletions catalog/app/containers/Admin/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const useToolbarStyles = M.makeStyles((t) => ({
},
actions: {
color: t.palette.text.secondary,
display: 'flex',
},
title: {
flex: '0 0 auto',
Expand Down
29 changes: 20 additions & 9 deletions catalog/app/containers/Admin/UsersAndRoles/Roles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import * as Form from '../Form'
import * as Table from '../Table'

import AttachedPolicies from './AttachedPolicies'
import SsoConfig from './SsoConfig'
import { MAX_POLICIES_PER_ROLE, getArnLink } from './shared'

import HAS_SSO_CONFIG_QUERY from './gql/HasSsoConfig.generated'
import ROLES_QUERY from './gql/Roles.generated'
import ROLE_CREATE_MANAGED_MUTATION from './gql/RoleCreateManaged.generated'
import ROLE_CREATE_UNMANAGED_MUTATION from './gql/RoleCreateUnmanaged.generated'
Expand Down Expand Up @@ -703,6 +705,9 @@ export default function Roles() {
const defaultRoleId = data.defaultRole?.id
const isDefaultRoleSettingDisabled = !!data.admin?.isDefaultRoleSettingDisabled

const ssoConfigData = GQL.useQueryS(HAS_SSO_CONFIG_QUERY)
const hasSsoConfig = !!ssoConfigData.admin?.ssoConfig

const filtering = Table.useFiltering({
rows,
filterBy: ({ name }) => name,
Expand All @@ -713,15 +718,21 @@ export default function Roles() {
})
const dialogs = Dialogs.use()

const toolbarActions = [
{
title: 'Create',
icon: <M.Icon>add</M.Icon>,
fn: React.useCallback(() => {
dialogs.open(({ close }) => <Create {...{ close }} />)
}, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps
},
]
const createAction = {
title: 'Create',
icon: <M.Icon>add</M.Icon>,
fn: React.useCallback(() => {
dialogs.open(({ close }) => <Create {...{ close }} />)
}, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps
}
const ssoConfigAction = {
title: 'SSO Config',
icon: <M.Icon>assignment_ind</M.Icon>,
fn: React.useCallback(() => {
dialogs.open(({ close }) => <SsoConfig {...{ close }} />)
}, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps
}
const toolbarActions = hasSsoConfig ? [ssoConfigAction, createAction] : [createAction]

const inlineActions = (role: Role) => [
role.arn
Expand Down
171 changes: 171 additions & 0 deletions catalog/app/containers/Admin/UsersAndRoles/SsoConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import * as FF from 'final-form'
import * as React from 'react'
import * as RF from 'react-final-form'
import * as M from '@material-ui/core'
import * as Lab from '@material-ui/lab'

import Lock from 'components/Lock'
import { loadMode } from 'components/FileEditor/loader'
import type * as Model from 'model'
import type * as Dialogs from 'utils/GlobalDialogs'
import * as GQL from 'utils/GraphQL'
import assertNever from 'utils/assertNever'
import * as validators from 'utils/validators'

import SET_SSO_CONFIG_MUTATION from './gql/SetSsoConfig.generated'
import SSO_CONFIG_QUERY from './gql/SsoConfig.generated'

const TextEditor = React.lazy(() => import('components/FileEditor/TextEditor'))

type TextFieldProps = RF.FieldRenderProps<string> & M.TextFieldProps

const TEXT_EDITOR_TYPE = { brace: 'yaml' as const }

const ERRORS = {
required: 'Enter an SSO config',
}

function TextField({ errors, input, meta }: TextFieldProps) {
// TODO: lint yaml
const errorMessage = meta.submitFailed && errors[meta.error]
return (
<TextEditor
error={errorMessage ? new Error(errorMessage) : null}
onChange={input.onChange}
type={TEXT_EDITOR_TYPE}
value={meta.initial}
/>
)
}

const useStyles = M.makeStyles((t) => ({
lock: {
bottom: t.spacing(6.5),
top: t.spacing(8),
},
error: {
marginTop: t.spacing(2),
},
}))

type FormValues = Record<'config', string>

interface FormProps {
formApi: RF.FormRenderProps<FormValues>
close: Dialogs.Close<string | void>
ssoConfig: Pick<Model.GQLTypes.SsoConfig, 'text'> | null
error: null | Error
}

function Form({
close,
error,
ssoConfig,
formApi: {
dirtySinceLastSubmit,
handleSubmit,
hasValidationErrors,
pristine,
submitFailed,
submitting,
},
}: FormProps) {
const classes = useStyles()
return (
<>
<M.DialogTitle disableTypography>
<M.Typography variant="h5">SSO role mapping config</M.Typography>
</M.DialogTitle>
<M.DialogContent>
<RF.Field
component={TextField}
errors={ERRORS}
initialValue={ssoConfig?.text}
label="SSO config"
name="config"
validate={validators.required as FF.FieldValidator<any>}
/>
{!!error && !dirtySinceLastSubmit && (
<Lab.Alert className={classes.error} severity="error">
{error.message}
</Lab.Alert>
)}
</M.DialogContent>
<M.DialogActions>
<M.Button onClick={() => close('cancel')} color="primary" disabled={submitting}>
Cancel
</M.Button>
<M.Button
color="primary"
disabled={pristine || submitting || (submitFailed && hasValidationErrors)}
onClick={handleSubmit}
>
Save
</M.Button>
</M.DialogActions>
{submitting && (
<Lock className={classes.lock}>
<M.CircularProgress size={80} />
</Lock>
)}
</>
)
}

interface DataProps {
children: (props: FormProps) => React.ReactNode
close: Dialogs.Close<string | void>
}

function Data({ children, close }: DataProps) {
const data = GQL.useQueryS(SSO_CONFIG_QUERY)
loadMode('yaml')
const setSsoConfig = GQL.useMutation(SET_SSO_CONFIG_MUTATION)
const [error, setError] = React.useState<null | Error>(null)

const onSubmit = React.useCallback(
async ({ config }: FormValues) => {
try {
if (!config) {
throw new Error('Enter an SSO config')
}
const {
admin: { setSsoConfig: r },
} = await setSsoConfig({ config })
switch (r.__typename) {
case 'Ok':
return close('submit')
case 'InvalidInput':
return setError(new Error('Unable to update SSO config'))
case 'OperationError':
return setError(new Error(`Unable to update SSO config: ${r.message}`))
default:
assertNever(r)
}
} catch (e) {
return setError(e instanceof Error ? e : new Error('Error updating SSO config'))
}
},
[close, setSsoConfig],
)

return (
<RF.Form onSubmit={onSubmit}>
{(formApi) =>
children({ formApi, close, error: error, ssoConfig: data.admin?.ssoConfig })
}
</RF.Form>
)
}

interface SuspendedProps {
close: Dialogs.Close<string | void>
}

export default function Suspended({ close }: SuspendedProps) {
return (
<React.Suspense fallback={<M.CircularProgress size={80} />}>
<Data close={close}>{(props) => <Form {...props} />}</Data>
</React.Suspense>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* 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 containers_Admin_UsersAndRoles_gql_HasSsoConfigQueryVariables = Types.Exact<{
[key: string]: never
}>

export type containers_Admin_UsersAndRoles_gql_HasSsoConfigQuery = {
readonly __typename: 'Query'
} & {
readonly admin: { readonly __typename: 'AdminQueries' } & {
readonly ssoConfig: Types.Maybe<
{ readonly __typename: 'SsoConfig' } & Pick<Types.SsoConfig, 'timestamp'>
>
}
}

export const containers_Admin_UsersAndRoles_gql_HasSsoConfigDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'containers_Admin_UsersAndRoles_gql_HasSsoConfig' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'admin' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'ssoConfig' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'timestamp' } },
],
},
},
],
},
},
],
},
},
],
} as unknown as DocumentNode<
containers_Admin_UsersAndRoles_gql_HasSsoConfigQuery,
containers_Admin_UsersAndRoles_gql_HasSsoConfigQueryVariables
>

export { containers_Admin_UsersAndRoles_gql_HasSsoConfigDocument as default }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query {
admin {
ssoConfig {
timestamp
}
}
}
Loading

0 comments on commit 66f7d0a

Please sign in to comment.