Merge pull request #14585 from overleaf/msm-sso-config-modal

[web] SSO Config modal

GitOrigin-RevId: e704afa61fe14390b64ce29a27ccbce7e884b396
This commit is contained in:
Miguel Serrano
2023-09-13 08:03:46 +00:00
committed by Copybot
parent 4b72908940
commit 0f30edf69f
10 changed files with 502 additions and 6 deletions
@@ -127,6 +127,7 @@
"category_operators": "",
"category_relations": "",
"center": "",
"certificate": "",
"change": "",
"change_currency": "",
"change_or_cancel-cancel": "",
@@ -178,6 +179,7 @@
"compile_terminated_by_user": "",
"compiler": "",
"compiling": "",
"configure_sso": "",
"confirm": "",
"confirm_affiliation": "",
"confirm_affiliation_to_relink_dropbox": "",
@@ -293,6 +295,7 @@
"edit_dictionary_empty": "",
"edit_dictionary_remove": "",
"edit_figure": "",
"edit_sso_configuration": "",
"edit_tag": "",
"editing": "",
"editing_captions": "",
@@ -311,6 +314,7 @@
"enabling": "",
"end_of_document": "",
"enter_image_url": "",
"entry_point": "",
"error": "",
"error_performing_request": "",
"example_project": "",
@@ -995,6 +999,7 @@
"showing_x_out_of_n_projects": "",
"showing_x_results": "",
"showing_x_results_of_total": "",
"signature_algorithm": "",
"single_sign_on_sso": "",
"something_went_wrong_loading_pdf_viewer": "",
"something_went_wrong_processing_the_request": "",
@@ -1007,6 +1012,11 @@
"sort_by_x": "",
"source": "",
"spell_check": "",
"sso_config_prop_help_certificate": "",
"sso_config_prop_help_first_name": "",
"sso_config_prop_help_last_name": "",
"sso_config_prop_help_user_entry_point": "",
"sso_config_prop_help_user_id": "",
"sso_explanation": "",
"sso_link_error": "",
"start_by_adding_your_email": "",
@@ -1204,6 +1214,9 @@
"used_when_referring_to_the_figure_elsewhere_in_the_document": "",
"user_deletion_error": "",
"user_deletion_password_reset_tip": "",
"user_first_name_attribute": "",
"user_id_attribute": "",
"user_last_name_attribute": "",
"user_sessions": "",
"validation_issue_entry_description": "",
"vat": "",
@@ -1,20 +1,38 @@
/* eslint-disable jsx-a11y/label-has-for */
/* eslint-disable jsx-a11y/label-has-associated-control */
import { useRef, useEffect } from 'react'
import classNames from 'classnames'
import { useSelect } from 'downshift'
import Icon from './icon'
import { useTranslation } from 'react-i18next'
type SelectProps<T> = {
export type SelectProps<T> = {
// The items rendered as dropdown options.
items: T[]
itemToString: (item: T | null) => string
// Stringifies an item of type T. The resulting string is rendered as a dropdown option.
itemToString: (item: T | null | undefined) => string
// Caption for the dropdown.
label?: string
// Attribute used to identify the component inside a Form. This name is used to
// retrieve FormData when the form is submitted. The value of the FormData entry
// is the string returned by `itemToString(selectedItem)`.
name?: string
// Hint text displayed in the initial render.
defaultText?: string
itemToSubtitle?: (item: T | null) => string
// Initial selected item, displayed in the initial render. When both `defaultText`
// and `defaultItem` are set the latter is ignored.
defaultItem?: T
// Stringifies an item. The resulting string is rendered as a subtitle in a dropdown option.
itemToSubtitle?: (item: T | null | undefined) => string
// Stringifies an item. The resulting string is rendered as a React `key` for each item.
itemToKey: (item: T) => string
// Callback invoked after the selected item is updated.
onSelectedItemChanged?: (item: T | null | undefined) => void
// When `true` item selection is disabled.
disabled?: boolean
// When `true` displays an "Optional" subtext after the `label` caption.
optionalLabel?: boolean
// When `true` displays a spinner next to the `label` caption.
loading?: boolean
}
@@ -22,7 +40,9 @@ export const Select = <T,>({
items,
itemToString = item => (item === null ? '' : String(item)),
label,
name,
defaultText = 'Items',
defaultItem,
itemToSubtitle,
itemToKey,
onSelectedItemChanged,
@@ -48,8 +68,37 @@ export const Select = <T,>({
}
},
})
const rootRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!name || !rootRef.current) return
const parentForm: HTMLFormElement | null | undefined =
rootRef.current?.closest('form')
if (!parentForm) return
function handleFormDataEvent(event: FormDataEvent) {
const data = event.formData
const key = name as string // can't be undefined due to early exit in the effect
if (selectedItem || defaultItem) {
data.append(key, itemToString(selectedItem || defaultItem))
}
}
parentForm.addEventListener('formdata', handleFormDataEvent)
return () => {
parentForm.removeEventListener('formdata', handleFormDataEvent)
}
}, [name, itemToString, selectedItem, defaultItem])
let value: string | undefined
if (selectedItem || defaultItem) {
value = itemToString(selectedItem || defaultItem)
} else {
value = defaultText
}
return (
<div className="select-wrapper">
<div className="select-wrapper" ref={rootRef}>
<div>
{label ? (
<label {...getLabelProps()}>
@@ -59,14 +108,14 @@ export const Select = <T,>({
({t('optional')})
</span>
)}{' '}
{loading && <Icon fw type="spinner" spin />}
{loading && <Icon data-testid="spinner" fw type="spinner" spin />}
</label>
) : null}
<div
className={classNames({ disabled }, 'select-trigger')}
{...getToggleButtonProps({ disabled })}
>
<div>{selectedItem ? itemToString(selectedItem) : defaultText}</div>
<div>{value}</div>
<div>
{isOpen ? (
<Icon type="chevron-up" fw />
@@ -0,0 +1,39 @@
import GroupSettings from '../../../../modules/managed-users/frontend/js/components/group-settings'
import { useMeta } from '../../hooks/use-meta'
export const GroupSettingsWithManagedUsersDisabledAndNoSSOFeature = () => {
useMeta({
'ol-managedUsersEnabled': false,
'ol-hasGroupSSOFeature': false,
})
return <GroupSettings />
}
export const GroupSettingsWithManagedUsersDisabledAndSSOFeature = () => {
useMeta({
'ol-managedUsersEnabled': false,
'ol-hasGroupSSOFeature': true,
})
return <GroupSettings />
}
export const GroupSettingsWithManagedUsersEnabledAndNoSSOFeature = () => {
useMeta({
'ol-managedUsersEnabled': true,
'ol-hasGroupSSOFeature': false,
})
return <GroupSettings />
}
export const GroupSettingsWithManagedUsersEnabledAndSSOFeature = () => {
useMeta({
'ol-managedUsersEnabled': true,
'ol-hasGroupSSOFeature': true,
})
return <GroupSettings />
}
export default {
title: 'Subscription / Managed Users',
component: GroupSettings,
}
@@ -0,0 +1,88 @@
import useFetchMock from '../../hooks/use-fetch-mock'
import { useMeta } from '../../hooks/use-meta'
import SSOConfigurationModal, {
SSOConfigurationModalProps,
} from '../../../../modules/managed-users/frontend/js/components/modals/sso-configuration-modal'
const config = {
entryPoint: 'http://idp.example.com/entry_point',
certificate:
'MIIDXTCCAkWgAwIBAgIJAOvOeQ4xFTzsMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMTE1MTQxMjU5WhcNMjYxMTE1MTQxMjU5WjBFMQswCQYDVQQGEwJHQjETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCT6MBe5G9VoLU8MfztOEbUhnwLp17ak8eFUqxqeXkkqtWB0b/cmIBU3xoQoO3dIF8PBzfqehqfYVhrNt/TFgcmDfmJnPJRL1RJWMW3VmiP5odJ3LwlkKbZpkeT3wZ8HEJIR1+zbpxiBNkbd2GbdR1iumcsHzMYX1A2CBj+ZMV5VijC+K4P0e9c05VsDEUtLmfeAasJAiumQoVVgAe/BpiXjICGGewa6EPFI7mKkifIRKOGxdRESwZZjxP30bI31oDN0cgKqIgSJtJ9nfCn9jgBMBkQHu42WMuaWD4jrGd7+vYdX+oIfArs9aKgAH5kUGhGdew2R9SpBefrhbNxG8QIDAQABo1AwTjAdBgNVHQ4EFgQU+aSojSyyLChP/IpZcafvSdhj7KkwHwYDVR0jBBgwFoAU+aSojSyyLChP/IpZcafvSdhj7KkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABl3+OOVLBWMKs6PjA8lPuloWDNzSr3v76oUcHqAb+cfbucjXrOVsS9RJ0X9yxvCQyfM9FfY43DbspnN3izYhdvbJD8kKLNf0LA5st+ZxLfy0ACyL2iyAwICaqndqxAjQYplFAHmpUiu1DiHckyBPekokDJd+ze95urHMOsaGS5RWPoKJVE0bkaAeZCmEu0NNpXRSBiuxXSTeSAJfv6kyE/rkdhzUKyUl/cGQFrsVYfAFQVA+W6CKOh74ErSEzSHQQYndl7nD33snD/YqdU1ROxV6aJzLKCg+sdj+wRXSP2u/UHnM4jW9TGJfhO42jzL6WVuEvr9q4l7zWzUQKKKhtQ==',
signatureAlgorithm: 'sha256',
userIdAttribute: 'email',
}
export const ConfigurationModalLoadingError = (
args: SSOConfigurationModalProps
) => {
useMeta({ 'ol-groupId': '123' })
useFetchMock(fetchMock => {
fetchMock.get(
'express:/manage/groups/:id/settings/sso',
{ status: 500 },
{
delay: 1000,
}
)
fetchMock.post('express:/manage/groups/:id/settings/sso', 200, {
delay: 500,
})
})
return <SSOConfigurationModal {...args} />
}
export const ConfigurationModal = (args: SSOConfigurationModalProps) => {
useMeta({ 'ol-groupId': '123' })
useFetchMock(fetchMock => {
fetchMock.get('express:/manage/groups/:id/settings/sso', config, {
delay: 500,
})
fetchMock.post('express:/manage/groups/:id/settings/sso', 200, {
delay: 500,
})
})
return <SSOConfigurationModal {...args} />
}
export const ConfigurationModalEmpty = (args: SSOConfigurationModalProps) => {
useMeta({ 'ol-groupId': '123' })
useFetchMock(fetchMock => {
fetchMock.get(
'express:/manage/groups/:id/settings/sso',
{},
{
delay: 500,
}
)
fetchMock.post('express:/manage/groups/:id/settings/sso', 200, {
delay: 500,
})
})
return <SSOConfigurationModal {...args} />
}
export const ConfigurationModalSaveError = (
args: SSOConfigurationModalProps
) => {
useMeta({ 'ol-groupId': '123' })
useFetchMock(fetchMock => {
fetchMock.get('express:/manage/groups/:id/settings/sso', config, {
delay: 500,
})
fetchMock.post('express:/manage/groups/:id/settings/sso', 500, {
delay: 1000,
})
})
return <SSOConfigurationModal {...args} />
}
export default {
title: 'Subscription / SSO',
component: SSOConfigurationModal,
args: {
show: true,
},
argTypes: {
handleHide: { action: 'close modal' },
},
}