diff --git a/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx b/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx index 0a88e280065..6284157f740 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/Roles.tsx @@ -29,7 +29,6 @@ import ROLE_UPDATE_UNMANAGED_MUTATION from './gql/RoleUpdateUnmanaged.generated' import ROLE_DELETE_MUTATION from './gql/RoleDelete.generated' import ROLE_SET_DEFAULT_MUTATION from './gql/RoleSetDefault.generated' import { RoleSelectionFragment as Role } from './gql/RoleSelection.generated' -import SSO_CONFIG_QUERY from './gql/SsoConfig.generated' const columns: Table.Column[] = [ { @@ -705,9 +704,6 @@ export default function Roles() { const defaultRoleId = data.defaultRole?.id const isDefaultRoleSettingDisabled = !!data.admin?.isDefaultRoleSettingDisabled - const ssoConfigData = GQL.useQueryS(SSO_CONFIG_QUERY) - const hasSsoConfig = !!ssoConfigData.admin?.ssoConfig - const filtering = Table.useFiltering({ rows, filterBy: ({ name }) => name, @@ -718,21 +714,22 @@ export default function Roles() { }) const dialogs = Dialogs.use() - const createAction = { - title: 'Create', - icon: add, - fn: React.useCallback(() => { - dialogs.open(({ close }) => ) - }, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps - } - const ssoConfigAction = { - title: 'SSO Config', - icon: assignment_ind, - fn: React.useCallback(() => { - dialogs.open(({ close }) => ) - }, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps - } - const toolbarActions = hasSsoConfig ? [ssoConfigAction, createAction] : [createAction] + const toolbarActions = [ + { + title: 'SSO Config', + icon: assignment_ind, + fn: React.useCallback(() => { + dialogs.open(({ close }) => ) + }, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps + }, + { + title: 'Create', + icon: add, + fn: React.useCallback(() => { + dialogs.open(({ close }) => ) + }, [dialogs.open]), // eslint-disable-line react-hooks/exhaustive-deps + }, + ] const inlineActions = (role: Role) => [ role.arn diff --git a/catalog/app/containers/Admin/UsersAndRoles/SsoConfig.tsx b/catalog/app/containers/Admin/UsersAndRoles/SsoConfig.tsx index a6df8091ebe..a10a4be4acb 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/SsoConfig.tsx +++ b/catalog/app/containers/Admin/UsersAndRoles/SsoConfig.tsx @@ -2,32 +2,41 @@ 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 { docs } from 'constants/urls' import type * as Model from 'model' import type * as Dialogs from 'utils/GlobalDialogs' import * as GQL from 'utils/GraphQL' +import StyledLink from 'utils/StyledLink' import assertNever from 'utils/assertNever' +import { mkFormError, mapInputErrors } from 'utils/formTools' import * as validators from 'utils/validators' +import { FormError } from '../Form' + 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')) +const TEXT_FIELD_ERRORS = { + required: 'Enter an SSO config', +} + +const FORM_ERRORS = { + unexpected: 'Unable to update SSO config: something went wrong', +} + type TextFieldProps = RF.FieldRenderProps & 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] + const error = meta.error || meta.submitError + const errorMessage = meta.submitFailed && error ? errors[error] || error : undefined return ( ({ - lock: { - bottom: t.spacing(6.5), - top: t.spacing(8), + delete: { + background: t.palette.error.light, + color: t.palette.error.contrastText, + marginRight: 'auto', + '&:hover': { + background: t.palette.error.main, + }, }, error: { marginTop: t.spacing(2), }, + lock: { + bottom: t.spacing(6.5), + top: t.spacing(8), + }, })) type FormValues = Record<'config', string> interface FormProps { - formApi: RF.FormRenderProps close: Dialogs.Close + formApi: RF.FormRenderProps + onDelete: () => Promise ssoConfig: Pick | null - error: null | Error } function Form({ close, - error, - ssoConfig, formApi: { - dirtySinceLastSubmit, + error, handleSubmit, hasValidationErrors, pristine, + submitError, submitFailed, submitting, }, + onDelete, + ssoConfig, }: FormProps) { const classes = useStyles() return ( @@ -79,19 +97,34 @@ function Form({ } /> - {!!error && !dirtySinceLastSubmit && ( - - {error.message} - + {submitFailed && ( + <> + + + Learn more about{' '} + + SSO permissions mapping + + . + + )} + + Delete + close('cancel')} color="primary" disabled={submitting}> Cancel @@ -121,38 +154,70 @@ 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) - const onSubmit = React.useCallback( - async ({ config }: FormValues) => { + const submitConfig = React.useCallback( + async (config: string | null) => { try { - if (!config) { - throw new Error('Enter an SSO config') - } const { admin: { setSsoConfig: r }, } = await setSsoConfig({ config }) + if (!r && !config) { + close('submit') + return undefined + } + if (!r) return assertNever(r as never) switch (r.__typename) { case 'SsoConfig': - return close('submit') + close('submit') + return undefined case 'InvalidInput': - return setError(new Error('Unable to update SSO config')) + return mapInputErrors(r.errors) case 'OperationError': - return setError(new Error(`Unable to update SSO config: ${r.message}`)) + return mkFormError(r.message) default: - assertNever(r) + return assertNever(r) } } catch (e) { - return setError(e instanceof Error ? e : new Error('Error updating SSO config')) + // eslint-disable-next-line no-console + console.error('Error updating SSO config') + // eslint-disable-next-line no-console + console.error(e) + return mkFormError('unexpected') } }, [close, setSsoConfig], ) + const onSubmit = React.useCallback( + ({ config }: FormValues) => (config ? submitConfig(config) : { config: 'required' }), + [submitConfig], + ) + const [deleting, setDeleting] = React.useState< + FF.SubmissionErrors | boolean | undefined + >() + const onDelete = React.useCallback(async (): Promise => { + setDeleting(true) + const errors = await submitConfig(null) + setDeleting(errors) + }, [submitConfig]) return ( {(formApi) => - children({ formApi, close, error: error, ssoConfig: data.admin?.ssoConfig }) + children({ + onDelete, + // eslint-disable-next-line no-nested-ternary + formApi: !deleting + ? formApi + : deleting === true + ? { ...formApi, submitting: true } + : { + ...formApi, + submitError: deleting[FF.FORM_ERROR] || formApi.submitError, + submitFailed: true, + }, + close, + ssoConfig: data.admin?.ssoConfig, + }) } ) @@ -164,7 +229,13 @@ interface SuspendedProps { export default function Suspended({ close }: SuspendedProps) { return ( - }> + + + + } + > {(props) =>
} ) diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.generated.ts b/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.generated.ts index d38a0f40af1..2c7949079e7 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.generated.ts +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.generated.ts @@ -4,14 +4,14 @@ import * as Types from '../../../../model/graphql/types.generated' export type containers_Admin_UsersAndRoles_gql_SetSsoConfigMutationVariables = Types.Exact<{ - config: Types.Scalars['String'] + config: Types.Maybe }> export type containers_Admin_UsersAndRoles_gql_SetSsoConfigMutation = { readonly __typename: 'Mutation' } & { readonly admin: { readonly __typename: 'AdminMutations' } & { - readonly setSsoConfig: + readonly setSsoConfig: Types.Maybe< | ({ readonly __typename: 'SsoConfig' } & Pick< Types.SsoConfig, 'timestamp' | 'text' @@ -28,6 +28,7 @@ export type containers_Admin_UsersAndRoles_gql_SetSsoConfigMutation = { Types.OperationError, 'message' >) + > } } @@ -42,10 +43,7 @@ export const containers_Admin_UsersAndRoles_gql_SetSsoConfigDocument = { { kind: 'VariableDefinition', variable: { kind: 'Variable', name: { kind: 'Name', value: 'config' } }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, - }, + type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } }, }, ], selectionSet: { diff --git a/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.graphql b/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.graphql index e94ce521c09..1c148eb3a11 100644 --- a/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.graphql +++ b/catalog/app/containers/Admin/UsersAndRoles/gql/SetSsoConfig.graphql @@ -1,4 +1,4 @@ -mutation ($config: String!) { +mutation ($config: String) { admin { setSsoConfig(config: $config) { __typename diff --git a/catalog/app/model/graphql/schema.generated.ts b/catalog/app/model/graphql/schema.generated.ts index 2249e62c327..7177acc5c80 100644 --- a/catalog/app/model/graphql/schema.generated.ts +++ b/catalog/app/model/graphql/schema.generated.ts @@ -101,12 +101,9 @@ export default { { name: 'setSsoConfig', type: { - kind: 'NON_NULL', - ofType: { - kind: 'UNION', - name: 'SetSsoConfigResult', - ofType: null, - }, + kind: 'UNION', + name: 'SetSsoConfigResult', + ofType: null, }, args: [ { diff --git a/catalog/app/model/graphql/types.generated.ts b/catalog/app/model/graphql/types.generated.ts index f38a6be54d5..14d2d201991 100644 --- a/catalog/app/model/graphql/types.generated.ts +++ b/catalog/app/model/graphql/types.generated.ts @@ -39,7 +39,7 @@ export interface AccessCounts { export interface AdminMutations { readonly __typename: 'AdminMutations' readonly user: UserAdminMutations - readonly setSsoConfig: SetSsoConfigResult + readonly setSsoConfig: Maybe } export interface AdminMutationssetSsoConfigArgs { diff --git a/catalog/app/utils/GraphQL/Provider.tsx b/catalog/app/utils/GraphQL/Provider.tsx index 329f652724d..500bdd62983 100644 --- a/catalog/app/utils/GraphQL/Provider.tsx +++ b/catalog/app/utils/GraphQL/Provider.tsx @@ -337,7 +337,10 @@ export default function GraphQLProvider({ children }: React.PropsWithChildren<{} R.evolve({ admin: { user: { list: rmUser } } }), ) } - if (result.admin?.setSsoConfig?.__typename === 'SsoConfig') { + if ( + result.admin?.setSsoConfig?.__typename === 'SsoConfig' || + result.admin?.setSsoConfig === null + ) { cache.invalidate({ __typename: 'Query' }, 'admin') cache.invalidate({ __typename: 'Query' }, 'roles') } diff --git a/catalog/app/utils/formTools.ts b/catalog/app/utils/formTools.ts index 3f4d9da0634..66399556d07 100644 --- a/catalog/app/utils/formTools.ts +++ b/catalog/app/utils/formTools.ts @@ -1,7 +1,9 @@ -import { FORM_ERROR } from 'final-form' +import { SubmissionErrors, FORM_ERROR } from 'final-form' import * as React from 'react' -export const mkFormError = (err: React.ReactNode) => ({ [FORM_ERROR]: err }) +export const mkFormError = (err: React.ReactNode): SubmissionErrors => ({ + [FORM_ERROR]: err, +}) export interface InputError { path: string | null diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8dfd30f09a7..5d6ee882a52 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -25,7 +25,7 @@ Entries inside each section should be ordered by type: ## Catalog, Lambdas * [Fixed] **SECURITY**: Remove `polyfill.io` references ([#4038](https://github.com/quiltdata/quilt/pull/4038)) * [Changed] Renamed "Admin settings" to "Admin" ([#4045](https://github.com/quiltdata/quilt/pull/4045)) -* [Added] Admin: Support SSO permissions mapping (SSO config editor, disable role assignment for SSO-mapped users) ([#4070](https://github.com/quiltdata/quilt/pull/4070)) +* [Added] Admin: Support SSO permissions mapping (SSO config editor, disable role assignment for SSO-mapped users) ([#4070](https://github.com/quiltdata/quilt/pull/4070), [#4097](https://github.com/quiltdata/quilt/pull/4097)) # 6.0.0a5 - 2024-06-25 ## Python API