Merge pull request #14585 from overleaf/msm-sso-config-modal
[web] SSO Config modal GitOrigin-RevId: e704afa61fe14390b64ce29a27ccbce7e884b396
This commit is contained in:
@@ -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 />
|
||||
|
||||
+39
@@ -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' },
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user