diff --git a/components/connection/CreateConnection.tsx b/components/connection/CreateConnection.tsx index c4ff48dcc..9ed894770 100644 --- a/components/connection/CreateConnection.tsx +++ b/components/connection/CreateConnection.tsx @@ -1,187 +1,44 @@ import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { - saveConnection, - fieldCatalogFilterByConnection, - renderFieldList, - useFieldCatalog, - excludeFallback, - type AdminPortalSSODefaults, - type FormObj, - type FieldCatalogItem, -} from './utils'; -import { mutate } from 'swr'; -import { ApiResponse } from 'types'; import { errorToast } from '@components/Toaster'; import { useTranslation } from 'next-i18next'; -import { ButtonPrimary } from '@components/ButtonPrimary'; import { LinkBack } from '@components/LinkBack'; - -function getInitialState(connectionType, fieldCatalog: FieldCatalogItem[]) { - const _state = {}; - - fieldCatalog.forEach(({ key, type, members, fallback, attributes: { connection } }) => { - let value; - if (connection && connection !== connectionType) { - return; - } - /** By default those fields which do not have a fallback.activateCondition will be excluded */ - if (typeof fallback === 'object' && typeof fallback.activateCondition !== 'function') { - return; - } - if (type === 'object') { - value = getInitialState(connectionType, members as FieldCatalogItem[]); - } - _state[key] = value ? value : ''; - }); - return _state; -} +import { CreateSSOConnection } from '@boxyhq/react-ui/sso'; +import { BOXYHQ_UI_CSS } from '@components/styles'; +import { AdminPortalSSODefaults } from '@lib/utils'; const CreateConnection = ({ - setupLinkToken, isSettingsView = false, adminPortalSSODefaults, }: { - setupLinkToken?: string; idpEntityID?: string; isSettingsView?: boolean; adminPortalSSODefaults?: AdminPortalSSODefaults; }) => { - const fieldCatalog = useFieldCatalog({ isSettingsView }); const { t } = useTranslation('common'); const router = useRouter(); - const [loading, setLoading] = useState(false); - // STATE: New connection type - const [newConnectionType, setNewConnectionType] = useState<'saml' | 'oidc'>('saml'); + const redirectUrl = isSettingsView ? '/admin/settings/sso-connection' : '/admin/sso-connection'; - const handleNewConnectionTypeChange = (event) => { - setNewConnectionType(event.target.value); - }; - - const connectionIsSAML = newConnectionType === 'saml'; - const connectionIsOIDC = newConnectionType === 'oidc'; - - const backUrl = setupLinkToken - ? null - : isSettingsView - ? '/admin/settings/sso-connection' - : '/admin/sso-connection'; - const redirectUrl = setupLinkToken - ? `/setup/${setupLinkToken}/sso-connection` - : isSettingsView - ? '/admin/settings/sso-connection' - : '/admin/sso-connection'; - const mutationUrl = setupLinkToken - ? `/api/setup/${setupLinkToken}/sso-connection` - : isSettingsView - ? '/api/admin/connections?isSystemSSO' - : '/api/admin/connections'; - - // FORM LOGIC: SUBMIT - const save = async (event: React.FormEvent) => { - event.preventDefault(); - - setLoading(true); - - await saveConnection({ - formObj: formObj, - connectionIsSAML: connectionIsSAML, - connectionIsOIDC: connectionIsOIDC, - setupLinkToken, - callback: async (rawResponse) => { - setLoading(false); - - const response: ApiResponse = await rawResponse.json(); - - if ('error' in response) { - errorToast(response.error.message); - return; - } - - if (rawResponse.ok) { - await mutate(mutationUrl); - router.replace(redirectUrl); - } - }, - }); - }; - - // STATE: FORM - const [formObj, setFormObj] = useState(() => - isSettingsView - ? { ...getInitialState(newConnectionType, fieldCatalog), ...adminPortalSSODefaults } - : { ...getInitialState(newConnectionType, fieldCatalog) } - ); - // Resync form state on save - useEffect(() => { - const _state = getInitialState(newConnectionType, fieldCatalog); - setFormObj(isSettingsView ? { ..._state, ...adminPortalSSODefaults } : _state); - }, [newConnectionType, fieldCatalog, isSettingsView, adminPortalSSODefaults]); - - // HANDLER: Track fallback display - const activateFallback = (key, fallbackKey) => { - setFormObj((cur) => { - const temp = { ...cur }; - delete temp[key]; - const fallbackItem = fieldCatalog.find(({ key }) => key === fallbackKey); - const fallbackItemVal = fallbackItem?.type === 'object' ? {} : ''; - return { ...temp, [fallbackKey]: fallbackItemVal }; - }); - }; + const backUrl = redirectUrl; return ( <> {backUrl && } -
-

- {t('create_sso_connection')} -

-
-
{t('select_sso_type')}:
-
-
- -
-
- -
-
-
-
-
- {fieldCatalog - .filter(fieldCatalogFilterByConnection(newConnectionType)) - .filter(({ attributes: { hideInSetupView } }) => (setupLinkToken ? !hideInSetupView : true)) - .filter(excludeFallback(formObj)) - .map(renderFieldList({ formObj, setFormObj, activateFallback }))} -
- - {t('save_changes')} - -
-
-
+

+ {t('create_sso_connection')} +

+
+ router.replace(redirectUrl)} + errorCallback={(errMessage) => errorToast(errMessage)} + classNames={BOXYHQ_UI_CSS} + />
); diff --git a/components/connection/EditConnection.tsx b/components/connection/EditConnection.tsx index 0c0748c23..f8f6b8962 100644 --- a/components/connection/EditConnection.tsx +++ b/components/connection/EditConnection.tsx @@ -1,56 +1,10 @@ import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { mutate } from 'swr'; - -import ConfirmationModal from '@components/ConfirmationModal'; -import { - saveConnection, - fieldCatalogFilterByConnection, - renderFieldList, - useFieldCatalog, - type FormObj, - type FieldCatalogItem, - excludeFallback, -} from './utils'; -import { ApiResponse } from 'types'; import { errorToast, successToast } from '@components/Toaster'; import { useTranslation } from 'next-i18next'; import { LinkBack } from '@components/LinkBack'; -import { ButtonPrimary } from '@components/ButtonPrimary'; -import { ButtonDanger } from '@components/ButtonDanger'; -import { isObjectEmpty } from '@lib/ui/utils'; -import { ToggleConnectionStatus } from './ToggleConnectionStatus'; import type { OIDCSSORecord, SAMLSSORecord } from '@boxyhq/saml-jackson'; - -function getInitialState(connection, fieldCatalog: FieldCatalogItem[], connectionType) { - const _state = {}; - - fieldCatalog.forEach(({ key, attributes, type, members }) => { - let value; - if (attributes.connection && attributes.connection !== connectionType) { - return; - } - if (type === 'object') { - value = getInitialState(connection, members as FieldCatalogItem[], connectionType); - if (isObjectEmpty(value)) { - return; - } - } else if (typeof attributes.accessor === 'function') { - if (attributes.accessor(connection) === undefined) { - return; - } - value = attributes.accessor(connection); - } else { - value = connection?.[key]; - } - _state[key] = value - ? attributes.isArray - ? value.join('\r\n') // render list of items on newline eg:- redirect URLs - : value - : ''; - }); - return _state; -} +import { EditSAMLConnection, EditOIDCConnection } from '@boxyhq/react-ui/sso'; +import { BOXYHQ_UI_CSS } from '@components/styles'; type EditProps = { connection: SAMLSSORecord | OIDCSSORecord; @@ -59,8 +13,6 @@ type EditProps = { }; const EditConnection = ({ connection, setupLinkToken, isSettingsView = false }: EditProps) => { - const fieldCatalog = useFieldCatalog({ isEditView: true, isSettingsView }); - const router = useRouter(); const { t } = useTranslation('common'); @@ -69,117 +21,16 @@ const EditConnection = ({ connection, setupLinkToken, isSettingsView = false }: const connectionIsSAML = 'idpMetadata' in connection && typeof connection.idpMetadata === 'object'; const connectionIsOIDC = 'oidcProvider' in connection && typeof connection.oidcProvider === 'object'; - // FORM LOGIC: SUBMIT - const save = async (event) => { - event.preventDefault(); - saveConnection({ - formObj: formObj, - connectionIsSAML: connectionIsSAML, - connectionIsOIDC: connectionIsOIDC, - isEditView: true, - setupLinkToken, - callback: async (res) => { - if (res.ok) { - successToast(t('saved')); - // revalidate on save - mutate( - setupLinkToken - ? `/api/setup/${setupLinkToken}/sso-connection` - : `/api/admin/connections/${connectionClientId}` - ); - return; - } - - const response: ApiResponse = await res.json(); - - if ('error' in response) { - errorToast(response.error.message); - return; - } - }, - }); - }; - - // LOGIC: DELETE - const [delModalVisible, setDelModalVisible] = useState(false); - const toggleDelConfirm = () => setDelModalVisible(!delModalVisible); - const deleteConnection = async () => { - const queryParams = new URLSearchParams({ - clientID: connection.clientID, - clientSecret: connection.clientSecret, - }); - const res = await fetch( - setupLinkToken - ? `/api/setup/${setupLinkToken}/sso-connection?${queryParams}` - : `/api/admin/connections?${queryParams}`, - { - method: 'DELETE', - } - ); - - const response: ApiResponse = await res.json(); - - toggleDelConfirm(); - - if ('error' in response) { - errorToast(response.error.message); - return; - } - - if (res.ok) { - await mutate( - setupLinkToken - ? `/api/setup/${setupLinkToken}/connections` - : isSettingsView - ? `/api/admin/connections?isSystemSSO` - : '/api/admin/connections' - ); - router.replace( - setupLinkToken - ? `/setup/${setupLinkToken}/sso-connection` - : isSettingsView - ? '/admin/settings/sso-connection' - : '/admin/sso-connection' - ); - } - }; - - const connectionType = connectionIsSAML ? 'saml' : connectionIsOIDC ? 'oidc' : null; - - // STATE: FORM - const [formObj, setFormObj] = useState(() => - getInitialState(connection, fieldCatalog, connectionType) - ); - // Resync form state on save - useEffect(() => { - const _state = getInitialState(connection, fieldCatalog, connectionType); - setFormObj(_state); - }, [connection, fieldCatalog, connectionType]); - - const filteredFieldsByConnection = fieldCatalog.filter(fieldCatalogFilterByConnection(connectionType)); - - const activateFallback = (activeKey, fallbackKey) => { - setFormObj((cur) => { - const temp = { ...cur }; - delete temp[activeKey]; - const fallbackItem = fieldCatalog.find(({ key }) => key === fallbackKey); - const fallbackItemVal = fallbackItem?.type === 'object' ? {} : ''; - return { ...temp, [fallbackKey]: fallbackItemVal }; - }); - }; - const backUrl = setupLinkToken ? `/setup/${setupLinkToken}` : isSettingsView ? '/admin/settings/sso-connection' : '/admin/sso-connection'; - const fieldsToHideInSetupView = ['forceAuthn', 'clientID', 'clientSecret', 'idpCertExpiry', 'idpMetadata']; - const readOnlyFields = filteredFieldsByConnection - .filter((field) => field.attributes.editable === false) - .filter(({ attributes: { hideInSetupView } }) => (setupLinkToken ? !hideInSetupView : true)) - .filter(excludeFallback(formObj)) - .filter((field) => (setupLinkToken ? !fieldsToHideInSetupView.includes(field.key) : true)); + const apiUrl = setupLinkToken ? `/api/setup/${setupLinkToken}/sso-connection` : `/api/admin/connections`; + const connectionFetchUrl = setupLinkToken + ? `/api/setup/${setupLinkToken}/sso-connection/${connectionClientId}` + : `/api/admin/connections/${connectionClientId}`; return ( <> @@ -189,55 +40,99 @@ const EditConnection = ({ connection, setupLinkToken, isSettingsView = false }:

{t('edit_sso_connection')}

-
-
-
-
-
0 ? 'lg:w-3/5' : ''}`}> - {filteredFieldsByConnection - .filter((field) => field.attributes.editable !== false) - .filter(({ attributes: { hideInSetupView } }) => (setupLinkToken ? !hideInSetupView : true)) - .filter(excludeFallback(formObj)) - .filter((field) => (setupLinkToken ? !fieldsToHideInSetupView.includes(field.key) : true)) - .map(renderFieldList({ isEditView: true, formObj, setFormObj, activateFallback }))} -
- {readOnlyFields.length > 0 && ( -
- {readOnlyFields.map( - renderFieldList({ isEditView: true, formObj, setFormObj, activateFallback }) - )} -
- )} -
-
- {t('save_changes')} -
-
- {connection?.clientID && connection.clientSecret && ( -
-
-
{t('delete_this_connection')}
-

{t('all_your_apps_using_this_connection_will_stop_working')}

-
- - {t('delete')} - -
+
+ {connectionIsSAML && ( + { + operation === 'UPDATE' + ? successToast(t('saved')) + : operation === 'DELETE' + ? successToast(t('sso_connection_deleted_successfully')) + : successToast(t('copied')); + if (operation !== 'COPY') { + router.replace( + setupLinkToken + ? `/setup/${setupLinkToken}/sso-connection` + : isSettingsView + ? '/admin/settings/sso-connection' + : '/admin/sso-connection' + ); + } + }} + errorCallback={(errMessage) => errorToast(errMessage)} + /> )} - - + {connectionIsOIDC && ( + { + operation === 'UPDATE' + ? successToast(t('saved')) + : operation === 'DELETE' + ? successToast(t('sso_connection_deleted_successfully')) + : successToast(t('copied')); + if (operation !== 'COPY') { + router.replace( + setupLinkToken + ? `/setup/${setupLinkToken}/sso-connection` + : isSettingsView + ? '/admin/settings/sso-connection' + : '/admin/sso-connection' + ); + } + }} + errorCallback={(errMessage) => errorToast(errMessage)} + /> + )} +
); diff --git a/components/connection/ToggleConnectionStatus.tsx b/components/connection/ToggleConnectionStatus.tsx deleted file mode 100644 index 42bf656c6..000000000 --- a/components/connection/ToggleConnectionStatus.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { OIDCSSORecord, SAMLSSORecord } from '@boxyhq/saml-jackson'; -import { errorToast, successToast } from '@components/Toaster'; -import { FC, useEffect, useState } from 'react'; -import type { ApiResponse } from 'types'; -import { useTranslation } from 'next-i18next'; -import { ConnectionToggle } from '@components/ConnectionToggle'; - -interface Props { - connection: SAMLSSORecord | OIDCSSORecord; - setupLinkToken?: string; -} - -export const ToggleConnectionStatus: FC = (props) => { - const { connection, setupLinkToken } = props; - - const { t } = useTranslation('common'); - const [active, setActive] = useState(!connection.deactivated); - - useEffect(() => { - setActive(!connection.deactivated); - }, [connection]); - - const updateConnectionStatus = async (active: boolean) => { - setActive(active); - - const body = { - clientID: connection?.clientID, - clientSecret: connection?.clientSecret, - tenant: connection?.tenant, - product: connection?.product, - deactivated: !active, - }; - - if ('idpMetadata' in connection) { - body['isSAML'] = true; - } else { - body['isOIDC'] = true; - } - - const res = await fetch( - setupLinkToken ? `/api/setup/${setupLinkToken}/sso-connection` : '/api/admin/connections', - { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - } - ); - - if (!res.ok) { - const response: ApiResponse = await res.json(); - - if ('error' in response) { - errorToast(response.error.message); - } - - return; - } - - if (body.deactivated) { - successToast(t('connection_deactivated')); - } else { - successToast(t('connection_activated')); - } - }; - - return ( - <> - - - ); -}; diff --git a/components/connection/fieldCatalog.ts b/components/connection/fieldCatalog.ts deleted file mode 100644 index 7c3b53eec..000000000 --- a/components/connection/fieldCatalog.ts +++ /dev/null @@ -1,272 +0,0 @@ -/** - * Edit view will have extra fields to render parsed metadata and other attributes. - * All fields are editable unless they have `editable` set to false. - * All fields are required unless they have `required` set to false. - * `accessor` - only used to set initial state and retrieve saved value. Useful when key is different from retrieved payload. - * `fallback` - use this key to activate a fallback catalog item that will take in the values. The fallback will be activated - * by means of a switch control in the UI that allows us to deactivate the fallback catalog item and revert to the main field. - */ - -import type { FieldCatalogItem } from './utils'; - -export const getCommonFields = ({ - isEditView, - isSettingsView, -}: { - isEditView?: boolean; - isSettingsView?: boolean; -}): FieldCatalogItem[] => [ - { - key: 'name', - label: 'Name', - type: 'text', - placeholder: 'MyApp', - attributes: { required: false, hideInSetupView: true, 'data-testid': 'name' }, - }, - { - key: 'description', - label: 'Description', - type: 'text', - placeholder: 'A short description not more than 100 characters', - attributes: { maxLength: 100, required: false, hideInSetupView: true }, - }, - { - key: 'tenant', - label: 'Tenant', - type: 'text', - placeholder: 'acme.com', - attributes: isEditView - ? { - editable: false, - hideInSetupView: true, - } - : { - editable: !isSettingsView, - hideInSetupView: true, - }, - }, - { - key: 'product', - label: 'Product', - type: 'text', - placeholder: 'demo', - attributes: isEditView - ? { - editable: false, - hideInSetupView: true, - } - : { - editable: !isSettingsView, - hideInSetupView: true, - }, - }, - { - key: 'redirectUrl', - label: 'Allowed redirect URLs (newline separated)', - type: 'textarea', - placeholder: 'http://localhost:3366', - attributes: { isArray: true, rows: 3, hideInSetupView: true, editable: !isSettingsView }, - }, - { - key: 'defaultRedirectUrl', - label: 'Default redirect URL', - type: 'url', - placeholder: 'http://localhost:3366/login/saml', - attributes: { hideInSetupView: true, editable: !isSettingsView }, - }, - { - key: 'oidcClientId', - label: 'Client ID [OIDC Provider]', - type: 'text', - placeholder: '', - attributes: { - 'data-testid': 'oidcClientId', - connection: 'oidc', - accessor: (o) => o?.oidcProvider?.clientId, - hideInSetupView: false, - }, - }, - { - key: 'oidcClientSecret', - label: 'Client Secret [OIDC Provider]', - type: 'password', - placeholder: '', - attributes: { - 'data-testid': 'oidcClientSecret', - connection: 'oidc', - accessor: (o) => o?.oidcProvider?.clientSecret, - hideInSetupView: false, - }, - }, - { - key: 'oidcDiscoveryUrl', - label: 'Well-known URL of OpenID Provider', - type: 'url', - placeholder: 'https://example.com/.well-known/openid-configuration', - attributes: { - 'data-testid': 'oidcDiscoveryUrl', - connection: 'oidc', - accessor: (o) => o?.oidcProvider?.discoveryUrl, - hideInSetupView: false, - }, - fallback: { - key: 'oidcMetadata', - activateCondition: (fieldValue) => !fieldValue, - switch: { - label: 'Missing the discovery URL? Click here to set the individual attributes', - 'data-testid': 'oidcDiscoveryUrl-fallback-switch', - }, - }, - }, - { - key: 'oidcMetadata', - type: 'object', - members: [ - { - key: 'issuer', - label: 'Issuer', - type: 'url', - attributes: { - accessor: (o) => o?.oidcProvider?.metadata?.issuer, - hideInSetupView: false, - 'data-testid': 'issuer', - }, - }, - { - key: 'authorization_endpoint', - label: 'Authorization Endpoint', - type: 'url', - attributes: { - accessor: (o) => o?.oidcProvider?.metadata?.authorization_endpoint, - hideInSetupView: false, - 'data-testid': 'authorization_endpoint', - }, - }, - { - key: 'token_endpoint', - label: 'Token endpoint', - type: 'url', - attributes: { - accessor: (o) => o?.oidcProvider?.metadata?.token_endpoint, - hideInSetupView: false, - 'data-testid': 'token_endpoint', - }, - }, - { - key: 'jwks_uri', - label: 'JWKS URI', - type: 'url', - attributes: { - accessor: (o) => o?.oidcProvider?.metadata?.jwks_uri, - hideInSetupView: false, - 'data-testid': 'jwks_uri', - }, - }, - { - key: 'userinfo_endpoint', - label: 'UserInfo endpoint', - type: 'url', - attributes: { - accessor: (o) => o?.oidcProvider?.metadata?.userinfo_endpoint, - hideInSetupView: false, - 'data-testid': 'userinfo_endpoint', - }, - }, - ], - attributes: { connection: 'oidc', hideInSetupView: false }, - fallback: { - key: 'oidcDiscoveryUrl', - switch: { - label: 'Have a discovery URL? Click here to set it', - 'data-testid': 'oidcMetadata-fallback-switch', - }, - }, - }, - { - key: 'rawMetadata', - label: `Raw IdP XML ${isEditView ? '(fully replaces the current one)' : ''}`, - type: 'textarea', - placeholder: 'Paste the raw XML here', - attributes: { - rows: 5, - required: false, - connection: 'saml', - hideInSetupView: false, - }, - }, - { - key: 'metadataUrl', - label: `Metadata URL ${isEditView ? '(fully replaces the current one)' : ''}`, - type: 'url', - placeholder: 'Paste the Metadata URL here', - attributes: { - required: false, - connection: 'saml', - hideInSetupView: false, - 'data-testid': 'metadataUrl', - }, - }, - { - key: 'sortOrder', - label: 'Sort Order', - type: 'number', - placeholder: '10', - attributes: { - required: false, - hideInSetupView: true, - }, - }, - { - key: 'forceAuthn', - label: 'Force Authentication', - type: 'checkbox', - attributes: { required: false, connection: 'saml', hideInSetupView: false }, - }, -]; - -export const EditViewOnlyFields: FieldCatalogItem[] = [ - { - key: 'idpMetadata', - label: 'IdP Metadata', - type: 'pre', - attributes: { - isArray: false, - rows: 10, - editable: false, - connection: 'saml', - hideInSetupView: false, - formatForDisplay: (value) => { - const obj = JSON.parse(JSON.stringify(value)); - delete obj.validTo; - return JSON.stringify(obj, null, 2); - }, - }, - }, - { - key: 'idpCertExpiry', - label: 'IdP Certificate Validity', - type: 'pre', - attributes: { - isHidden: (value): boolean => !value || new Date(value).toString() == 'Invalid Date', - rows: 10, - editable: false, - connection: 'saml', - hideInSetupView: false, - accessor: (o) => o?.idpMetadata?.validTo, - showWarning: (value) => new Date(value) < new Date(), - formatForDisplay: (value) => new Date(value).toString(), - }, - }, - { - key: 'clientID', - label: 'Client ID', - type: 'text', - attributes: { editable: false, hideInSetupView: false }, - }, - { - key: 'clientSecret', - label: 'Client Secret', - type: 'password', - attributes: { editable: false, hideInSetupView: false }, - }, -]; diff --git a/components/connection/utils.tsx b/components/connection/utils.tsx deleted file mode 100644 index eafead97e..000000000 --- a/components/connection/utils.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import { ButtonLink } from '@components/ButtonLink'; -import { Dispatch, FormEvent, SetStateAction, useMemo, useState } from 'react'; -import { EditViewOnlyFields, getCommonFields } from './fieldCatalog'; -import { CopyToClipboardButton } from '@components/ClipboardButton'; -import EyeIcon from '@heroicons/react/24/outline/EyeIcon'; -import EyeSlashIcon from '@heroicons/react/24/outline/EyeSlashIcon'; -import { IconButton } from '@components/IconButton'; -import { useTranslation } from 'next-i18next'; - -export const saveConnection = async ({ - formObj, - isEditView, - connectionIsSAML, - connectionIsOIDC, - setupLinkToken, - callback, -}: { - formObj: FormObj; - isEditView?: boolean; - connectionIsSAML: boolean; - connectionIsOIDC: boolean; - setupLinkToken?: string; - callback: (res: Response) => Promise; -}) => { - const { - rawMetadata, - redirectUrl, - oidcDiscoveryUrl, - oidcMetadata, - oidcClientId, - oidcClientSecret, - metadataUrl, - ...rest - } = formObj; - - const encodedRawMetadata = btoa((rawMetadata as string) || ''); - const redirectUrlList = (redirectUrl as string)?.split(/\r\n|\r|\n/); - - const res = await fetch( - setupLinkToken ? `/api/setup/${setupLinkToken}/sso-connection` : '/api/admin/connections', - { - method: isEditView ? 'PATCH' : 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - ...rest, - encodedRawMetadata: connectionIsSAML ? encodedRawMetadata : undefined, - oidcDiscoveryUrl: connectionIsOIDC ? oidcDiscoveryUrl : undefined, - oidcMetadata: connectionIsOIDC ? oidcMetadata : undefined, - oidcClientId: connectionIsOIDC ? oidcClientId : undefined, - oidcClientSecret: connectionIsOIDC ? oidcClientSecret : undefined, - redirectUrl: redirectUrl && redirectUrlList ? JSON.stringify(redirectUrlList) : undefined, - metadataUrl: connectionIsSAML ? metadataUrl : undefined, - }), - } - ); - callback(res); -}; - -export function fieldCatalogFilterByConnection(connection) { - return ({ attributes }) => - attributes.connection && connection !== null ? attributes.connection === connection : true; -} - -/** If a field item has a fallback attribute, only render it if the form state has the field item */ -export function excludeFallback(formObj: FormObj) { - return ({ key, fallback }: FieldCatalogItem) => { - if (typeof fallback === 'object') { - if (!(key in formObj)) { - return false; - } - } - return true; - }; -} - -function getHandleChange( - setFormObj: Dispatch>, - opts: { key?: string; formObjParentKey?: string } = {} -) { - return (event: FormEvent) => { - const target = event.target as HTMLInputElement | HTMLTextAreaElement; - setFormObj((cur) => - opts.formObjParentKey - ? { - ...cur, - [opts.formObjParentKey]: { - ...(cur[opts.formObjParentKey] as FormObj), - [target.id]: target[opts.key || 'value'], - }, - } - : { ...cur, [target.id]: target[opts.key || 'value'] } - ); - }; -} - -type fieldAttributes = { - required?: boolean; - maxLength?: number; - editable?: boolean; - isArray?: boolean; - rows?: number; - accessor?: (any) => unknown; - formatForDisplay?: (value) => string; - isHidden?: (value) => boolean; - showWarning?: (value) => boolean; - hideInSetupView: boolean; - connection?: string; - 'data-testid'?: string; -}; - -export type FieldCatalogItem = { - key: string; - label?: string; - type: 'url' | 'object' | 'pre' | 'text' | 'password' | 'textarea' | 'checkbox' | 'number'; - placeholder?: string; - attributes: fieldAttributes; - members?: FieldCatalogItem[]; - fallback?: { - key: string; - activateCondition?: (fieldValue) => boolean; - switch: { label: string; 'data-testid'?: string }; - }; -}; - -export type AdminPortalSSODefaults = { - tenant: string; - product: string; - redirectUrl: string; - defaultRedirectUrl: string; -}; - -type FormObjValues = string | boolean | string[]; - -export type FormObj = Record>; - -export const useFieldCatalog = ({ - isEditView, - isSettingsView, -}: { - isEditView?: boolean; - isSettingsView?: boolean; -}) => { - const fieldCatalog = useMemo(() => { - if (isEditView) { - return [...getCommonFields({ isEditView: true, isSettingsView }), ...EditViewOnlyFields]; - } - return [...getCommonFields({ isSettingsView })]; - }, [isEditView, isSettingsView]); - return fieldCatalog; -}; - -function SecretInputFormControl({ - label, - value, - isHiddenClassName, - id, - placeholder, - required, - maxLength, - readOnly, - args, - dataTestId, -}) { - const { t } = useTranslation('common'); - const [isSecretShown, setisSecretShown] = useState(false); - return ( -
-
- -
- { - setisSecretShown(!isSecretShown); - }} - /> - -
-
- -
- ); -} - -export function renderFieldList(args: { - isEditView?: boolean; - formObj: FormObj; - setFormObj: Dispatch>; - formObjParentKey?: string; - activateFallback: (activeKey, fallbackKey) => void; -}) { - const FieldList = ({ - key, - placeholder, - label, - type, - members, - attributes: { - isHidden, - isArray, - rows, - formatForDisplay, - editable, - maxLength, - showWarning, - required = true, - 'data-testid': dataTestId, - }, - fallback, - }: FieldCatalogItem) => { - const readOnly = editable === false; - const value = - readOnly && typeof formatForDisplay === 'function' - ? formatForDisplay( - args.formObjParentKey ? args.formObj[args.formObjParentKey]?.[key] : args.formObj[key] - ) - : args.formObjParentKey - ? args.formObj[args.formObjParentKey]?.[key] - : args.formObj[key]; - - if (type === 'object') { - return ( -
- {typeof fallback === 'object' && - (typeof fallback.activateCondition === 'function' ? fallback.activateCondition(value) : true) && ( - { - /** Switch to fallback.key*/ - args.activateFallback(key, fallback.key); - }}> - {fallback.switch.label} - - )} - {members?.map( - renderFieldList({ - ...args, - formObjParentKey: key, - }) - )} -
- ); - } - - const isHiddenClassName = - typeof isHidden === 'function' && isHidden(args.formObj[key]) == true ? ' hidden' : ''; - - if (type === 'password') { - return ( - - ); - } - - return ( -
- {type !== 'checkbox' && ( -
- - {typeof fallback === 'object' && - (typeof fallback.activateCondition === 'function' - ? fallback.activateCondition(value) - : true) && ( - { - /** Switch to fallback.key*/ - args.activateFallback(key, fallback.key); - }}> - {fallback.switch.label} - - )} -
- )} - {type === 'pre' ? ( -
-            {value}
-          
- ) : type === 'textarea' ? ( -