jackson/components/connection/utils.tsx

389 lines
12 KiB
TypeScript

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<void>;
}) => {
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<SetStateAction<FormObj>>,
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<string, FormObjValues | Record<string, FormObjValues>>;
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 (
<div className='mb-6'>
<div className='flex items-center justify-between'>
<label
htmlFor={id}
className={'mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300' + isHiddenClassName}>
{label}
</label>
<div className='flex'>
<IconButton
tooltip={isSecretShown ? t('hide_secret') : t('show_secret')}
Icon={isSecretShown ? EyeSlashIcon : EyeIcon}
className='hover:text-primary mr-2'
onClick={() => {
setisSecretShown(!isSecretShown);
}}
/>
<CopyToClipboardButton text={value} />
</div>
</div>
<input
id={id}
type={isSecretShown ? 'text' : 'password'}
placeholder={placeholder}
value={(value as string) || ''}
required={required}
readOnly={readOnly}
maxLength={maxLength}
onChange={getHandleChange(args.setFormObj, { formObjParentKey: args.formObjParentKey })}
className={'input-bordered input w-full' + isHiddenClassName + (readOnly ? ' bg-gray-50' : '')}
data-testid={dataTestId}
/>
</div>
);
}
export function renderFieldList(args: {
isEditView?: boolean;
formObj: FormObj;
setFormObj: Dispatch<SetStateAction<FormObj>>;
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 (
<div key={key}>
{typeof fallback === 'object' &&
(typeof fallback.activateCondition === 'function' ? fallback.activateCondition(value) : true) && (
<ButtonLink
className='mb-2 px-0'
type='button'
data-testid={fallback.switch['data-testid']}
onClick={() => {
/** Switch to fallback.key*/
args.activateFallback(key, fallback.key);
}}>
{fallback.switch.label}
</ButtonLink>
)}
{members?.map(
renderFieldList({
...args,
formObjParentKey: key,
})
)}
</div>
);
}
const isHiddenClassName =
typeof isHidden === 'function' && isHidden(args.formObj[key]) == true ? ' hidden' : '';
if (type === 'password') {
return (
<SecretInputFormControl
key={key}
label={label}
value={value}
isHiddenClassName={isHiddenClassName}
id={key}
placeholder={placeholder}
required={required}
maxLength={maxLength}
readOnly={readOnly}
args={args}
dataTestId={dataTestId}
/>
);
}
return (
<div className='mb-6 ' key={key}>
{type !== 'checkbox' && (
<div className='flex items-center justify-between'>
<label
htmlFor={key}
className={
'mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300' + isHiddenClassName
}>
{label}
</label>
{typeof fallback === 'object' &&
(typeof fallback.activateCondition === 'function'
? fallback.activateCondition(value)
: true) && (
<ButtonLink
className='mb-2 px-0'
type='button'
data-testid={fallback.switch['data-testid']}
onClick={() => {
/** Switch to fallback.key*/
args.activateFallback(key, fallback.key);
}}>
{fallback.switch.label}
</ButtonLink>
)}
</div>
)}
{type === 'pre' ? (
<pre
className={
'block w-full overflow-auto rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500' +
isHiddenClassName +
(typeof showWarning === 'function' && showWarning(args.formObj[key])
? ' border-2 border-rose-500'
: '')
}
data-testid={dataTestId}>
{value}
</pre>
) : type === 'textarea' ? (
<textarea
id={key}
placeholder={placeholder}
value={(value as string) || ''}
required={required}
readOnly={readOnly}
maxLength={maxLength}
onChange={getHandleChange(args.setFormObj, { formObjParentKey: args.formObjParentKey })}
className={
'textarea-bordered textarea h-24 w-full' +
(isArray ? ' whitespace-pre' : '') +
isHiddenClassName +
(readOnly ? ' bg-gray-50' : '')
}
rows={rows}
data-testid={dataTestId}
/>
) : type === 'checkbox' ? (
<>
<label
htmlFor={key}
className={
'inline-block align-middle text-sm font-medium text-gray-900 dark:text-gray-300' +
isHiddenClassName
}>
{label}
</label>
<input
id={key}
type={type}
checked={!!value}
required={required}
readOnly={readOnly}
maxLength={maxLength}
onChange={getHandleChange(args.setFormObj, {
key: 'checked',
formObjParentKey: args.formObjParentKey,
})}
className={'checkbox-primary checkbox ml-5 align-middle' + isHiddenClassName}
data-testid={dataTestId}
/>
</>
) : (
<input
id={key}
type={type}
placeholder={placeholder}
value={(value as string) || ''}
required={required}
readOnly={readOnly}
maxLength={maxLength}
onChange={getHandleChange(args.setFormObj, { formObjParentKey: args.formObjParentKey })}
className={'input-bordered input w-full' + isHiddenClassName + (readOnly ? ' bg-gray-50' : '')}
data-testid={dataTestId}
/>
)}
</div>
);
};
return FieldList;
}