1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(ui) sso data mapping - by type

This commit is contained in:
Sylvain 2022-04-11 13:19:07 +02:00
parent 6ac3ad4373
commit d542292dbf
18 changed files with 296 additions and 45 deletions

View File

@ -85,7 +85,8 @@ require('src/javascript/plugins.js.erb');
function importAll (r) { r.keys().forEach(r); }
importAll(require.context('src/javascript/components/', true, /.*/));
// we do not include markdown files (*.md)
importAll(require.context('src/javascript/components/', true, /^.+\.(?!md).+/));
importAll(require.context('src/javascript/controllers/', true, /.*/));
importAll(require.context('src/javascript/services/', true, /.*/));
importAll(require.context('src/javascript/directives/', true, /.*/));

View File

@ -0,0 +1,16 @@
# components
This directory is holding the components built with [React](https://reactjs.org/).
During the migration phase, these components may be included in [the legacy angularJS app](../../templates) using [react2angular](https://github.com/coatue-oss/react2angular).
These components must be written using the following conventions:
- The component name must be in CamelCase.
- The component must be exported as a named export (no `export default`).
- A component `FooBar` must have a `className="foo-bar"` attribute on its top-level element.
- The stylesheet associated with the component must be located in `app/frontend/src/stylesheets/modules/same-directory-structure/foo-bar.scss`.
- All methods in the component must be commented with a comment block.
- Other constants and variables must be commented with an inline block.
- Depending on if we want to use the `<Suspense>` wrapper or not, we can export the component directly or wrap it in a `<Loader>` wrapper.
- When a component is used in angularJS, the wrapped is required. The component must be named like `const Foo` (no export if not used in React) and must have a `const FooWrapper` at the end of its file, which wraps the component in a `<Loader>`.

View File

@ -0,0 +1,28 @@
import React from 'react';
import { UseFormRegister } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FormInput } from '../form/form-input';
export interface BooleanMappingFormProps<TFieldValues> {
register: UseFormRegister<TFieldValues>,
fieldMappingId: number,
}
export const BooleanMappingForm = <TFieldValues extends FieldValues>({ register, fieldMappingId }: BooleanMappingFormProps<TFieldValues>) => {
const { t } = useTranslation('shared');
return (
<div className="boolean-mapping-form">
<h4>{t('app.shared.authentication.mappings')}</h4>
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.true_value`}
register={register}
rules={{ required: true }}
label={t('app.shared.authentication.true_value')} />
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.false_value`}
register={register}
rules={{ required: true }}
label={t('app.shared.authentication.false_value')} />
</div>
);
};

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { UseFormRegister, useFieldArray, ArrayPath, useWatch, Path } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import AuthProviderAPI from '../../api/auth-provider';
import { MappingFields } from '../../models/authentication-provider';
import { MappingFields, mappingType } from '../../models/authentication-provider';
import { Control } from 'react-hook-form/dist/types/form';
import { FormSelect } from '../form/form-select';
import { FormInput } from '../form/form-input';
@ -66,7 +66,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
/**
* Return the type of data expected for the given index, in the current data-mapping form
*/
const getDataType = (formData: Array<TFieldValues>, index: number): string => {
const getDataType = (formData: Array<TFieldValues>, index: number): mappingType => {
const model = getModel(formData, index);
const field = getField(formData, index);
if (model && field) {
@ -89,7 +89,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
}, []);
return (
<div className="data-mapping-form">
<div className="data-mapping-form array-mapping-form">
<h4>{t('app.shared.oauth2.define_the_fields_mapping')}</h4>
<FabButton
icon={<i className="fa fa-plus"/>}
@ -97,23 +97,46 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
{t('app.shared.oauth2.add_a_match')}
</FabButton>
{fields.map((item, index) => (
<div key={item.id} className="data-mapping-item">
<div key={item.id} className="mapping-item">
<div className="inputs">
<FormInput id={`auth_provider_mappings_attributes.${index}.id`} register={register} type="hidden" />
<div className="local-data">
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_model`} control={control} rules={{ required: true }} options={buildModelOptions()} label={t('app.shared.oauth2.model')}/>
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_field`} options={buildFieldOptions(output, index)} control={control} rules={{ required: true }} label={t('app.shared.oauth2.field')} />
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_model`}
control={control} rules={{ required: true }}
options={buildModelOptions()} label={t('app.shared.oauth2.model')}/>
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_field`}
options={buildFieldOptions(output, index)}
control={control}
rules={{ required: true }}
label={t('app.shared.oauth2.field')} />
</div>
<div className="remote-data">
<FormInput id={`auth_provider_mappings_attributes.${index}.api_endpoint`} register={register} rules={{ required: true }} placeholder="/api/resource..." label={t('app.shared.oauth2.api_endpoint_url')} />
<FormSelect id={`auth_provider_mappings_attributes.${index}.api_data_type`} options={[{ label: 'JSON', value: 'json' }]} control={control} rules={{ required: true }} label={t('app.shared.oauth2.api_type')} />
<FormInput id={`auth_provider_mappings_attributes.${index}.api_field`} register={register} rules={{ required: true }} placeholder="field_name..." label={t('app.shared.oauth2.api_fields')} />
<FormInput id={`auth_provider_mappings_attributes.${index}.api_endpoint`}
register={register}
rules={{ required: true }}
placeholder="/api/resource..."
label={t('app.shared.oauth2.api_endpoint_url')} />
<FormSelect id={`auth_provider_mappings_attributes.${index}.api_data_type`}
options={[{ label: 'JSON', value: 'json' }]}
control={control} rules={{ required: true }}
label={t('app.shared.oauth2.api_type')} />
<FormInput id={`auth_provider_mappings_attributes.${index}.api_field`}
register={register}
rules={{ required: true }}
placeholder="field_name..."
label={t('app.shared.oauth2.api_fields')} />
</div>
</div>
<div className="actions">
<FabButton icon={<i className="fa fa-random" />} onClick={toggleTypeMappingModal} />
<FabButton icon={<i className="fa fa-random" />} onClick={toggleTypeMappingModal} disabled={getField(output, index) === undefined} />
<FabButton icon={<i className="fa fa-trash" />} onClick={() => remove(index)} className="delete-button" />
<TypeMappingModal model={getModel(output, index)} field={getField(output, index)} type={getDataType(output, index)} isOpen={isOpenTypeMappingModal} toggleModal={toggleTypeMappingModal} />
<TypeMappingModal model={getModel(output, index)}
field={getField(output, index)}
type={getDataType(output, index)}
isOpen={isOpenTypeMappingModal}
toggleModal={toggleTypeMappingModal}
control={control} register={register}
fieldMappingId={index} />
</div>
</div>
))}

View File

@ -0,0 +1,49 @@
import React from 'react';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FormSelect } from '../form/form-select';
import { Control } from 'react-hook-form/dist/types/form';
export interface DateMappingFormProps<TFieldValues, TContext extends object> {
control: Control<TFieldValues, TContext>,
fieldMappingId: number,
}
export const DateMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ control, fieldMappingId }: DateMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('shared');
// available date formats
const dateFormats = [
{
label: 'ISO 8601',
value: 'iso8601'
},
{
label: 'RFC 2822',
value: 'rfc2822'
},
{
label: 'RFC 3339',
value: 'rfc3339'
},
{
label: 'Timestamp (s)',
value: 'timestamp-s'
},
{
label: 'Timestamp (ms)',
value: 'timestamp-ms'
}
];
return (
<div className="date-mapping-form">
<h4>{t('app.shared.authentication.input_format')}</h4>
<FormSelect id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.format`}
control={control}
rules={{ required: true }}
options={dateFormats}
label={t('app.shared.authentication.date_format')} />
</div>
);
};

View File

@ -0,0 +1,46 @@
import React from 'react';
import { ArrayPath, useFieldArray, UseFormRegister } from 'react-hook-form';
import { Control } from 'react-hook-form/dist/types/form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button';
import { FormInput } from '../form/form-input';
export interface IntegerMappingFormProps<TFieldValues, TContext extends object> {
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
fieldMappingId: number,
}
export const IntegerMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, fieldMappingId }: IntegerMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('shared');
const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath<TFieldValues> });
return (
<div className="integer-mapping-form array-mapping-form">
<h4>{t('app.shared.authentication.mappings')}</h4>
<FabButton
icon={<i className="fa fa-plus" />}
onClick={() => append({})} />
{fields.map((item, index) => (
<div key={item.id} className="mapping-item">
<div className="inputs">
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.from`}
register={register}
rules={{ required: true }}
label={t('app.shared.authentication.mapping_from')} />
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.to`}
register={register}
type="number"
rules={{ required: true }}
label={t('app.shared.authentication.mapping_to')} />
</div>
<div className="actions">
<FabButton icon={<i className="fa fa-trash" />} onClick={() => remove(index)} className="delete-button" />
</div>
</div>
))}
</div>
);
};

View File

@ -0,0 +1,45 @@
import React from 'react';
import { ArrayPath, useFieldArray, UseFormRegister } from 'react-hook-form';
import { Control } from 'react-hook-form/dist/types/form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button';
import { FormInput } from '../form/form-input';
export interface StringMappingFormProps<TFieldValues, TContext extends object> {
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
fieldMappingId: number,
}
export const StringMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, fieldMappingId }: StringMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('shared');
const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath<TFieldValues> });
return (
<div className="string-mapping-form array-mapping-form">
<h4>{t('app.shared.authentication.mappings')}</h4>
<FabButton
icon={<i className="fa fa-plus" />}
onClick={() => append({})} />
{fields.map((item, index) => (
<div key={item.id} className="mapping-item">
<div className="inputs">
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.from`}
register={register}
rules={{ required: true }}
label={t('app.shared.authentication.mapping_from')} />
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.to`}
register={register}
rules={{ required: true }}
label={t('app.shared.authentication.mapping_to')} />
</div>
<div className="actions">
<FabButton icon={<i className="fa fa-trash" />} onClick={() => remove(index)} className="delete-button" />
</div>
</div>
))}
</div>
);
};

View File

@ -1,20 +1,40 @@
import React from 'react';
import { FabModal } from '../base/fab-modal';
import { useTranslation } from 'react-i18next';
import { IntegerMappingForm } from './integer-mapping-form';
import { UseFormRegister } from 'react-hook-form';
import { Control } from 'react-hook-form/dist/types/form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { mappingType } from '../../models/authentication-provider';
import { BooleanMappingForm } from './boolean-mapping-form';
import { DateMappingForm } from './date-mapping-form';
import { StringMappingForm } from './string-mapping-form';
interface TypeMappingModalProps {
interface TypeMappingModalProps<TFieldValues, TContext extends object> {
model: string,
field: string,
type: string,
type: mappingType,
isOpen: boolean,
toggleModal: () => void,
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
fieldMappingId: number,
}
export const TypeMappingModal: React.FC<TypeMappingModalProps> = ({ model, field, type, isOpen, toggleModal }) => {
export const TypeMappingModal = <TFieldValues extends FieldValues, TContext extends object>({ model, field, type, isOpen, toggleModal, register, control, fieldMappingId }:TypeMappingModalProps<TFieldValues, TContext>) => {
const { t } = useTranslation('shared');
return (
<FabModal isOpen={isOpen} toggleModal={toggleModal}>
<h1>{model}</h1>
<h2>{field}</h2>
<h3>{type}</h3>
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
title={t('app.shared.authentication.data_mapping')}
confirmButton={<i className="fa fa-check" />}
onConfirm={toggleModal}>
<span>{model} &gt; {field} ({t('app.shared.authentication.TYPE_expected', { TYPE: t(`app.shared.authentication.types.${type}`) })})</span>
{type === 'integer' && <IntegerMappingForm register={register} control={control} fieldMappingId={fieldMappingId} />}
{type === 'boolean' && <BooleanMappingForm register={register} fieldMappingId={fieldMappingId} />}
{type === 'date' && <DateMappingForm control={control} fieldMappingId={fieldMappingId} />}
{type === 'string' && <StringMappingForm register={register} control={control} fieldMappingId={fieldMappingId} />}
</FabModal>
);
};

View File

@ -0,0 +1,8 @@
# components/from
This directory is holding the inputs components for usage within forms controlled by [React-hook-form](https://react-hook-form.com/).
All these components must have [props](https://reactjs.org/docs/components-and-props.html) that inherits from [FormComponent](../models/form-component.ts)
or from [FormControlledComponent](../models/form-component.ts).
Please look at the existing components for examples.

View File

@ -19,7 +19,7 @@ interface FormInputProps<TFieldValues> extends InputHTMLAttributes<HTMLInputElem
export const FormInput = <TFieldValues extends FieldValues>({ id, register, label, tooltip, defaultValue, icon, className, rules, readOnly, disabled, type, addOn, addOnClassName, placeholder, error, step }: FormInputProps<TFieldValues>) => {
// Compose classnames from props
const classNames = `
form-item ${className || ''}
form-input form-item ${className || ''}
${type === 'hidden' ? 'is-hidden' : ''}
${error && error[id] ? 'is-incorrect' : ''}
${rules && rules.required ? 'is-required' : ''}

View File

@ -29,7 +29,7 @@ type selectOption<TOptionValue> = { value: TOptionValue, label: string };
*/
export const FormMultiSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
const classNames = `
form-item ${className || ''}
form-multi-select form-item ${className || ''}
${error && error[id] ? 'is-incorrect' : ''}
${rules && rules.required ? 'is-required' : ''}
${disabled ? 'is-disabled' : ''}`;

View File

@ -28,7 +28,7 @@ type selectOption<TOptionValue> = { value: TOptionValue, label: string };
*/
export const FormSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, className, control, placeholder, options, valueDefault, error, rules, disabled, onChange }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
const classNames = `
form-item ${className || ''}
form-select form-item ${className || ''}
${error && error[id] ? 'is-incorrect' : ''}
${rules && rules.required ? 'is-required' : ''}
${disabled ? 'is-disabled' : ''}`;

View File

@ -24,7 +24,7 @@ export interface AuthenticationProviderMapping {
false_value: string,
mapping: {
from: string,
to: number
to: number|string
}
}
}

View File

@ -25,6 +25,7 @@
@import "modules/stripe";
@import "modules/tour";
@import "modules/authentication-provider/data-mapping-form";
@import "modules/authentication-provider/array-mapping-form";
@import "modules/base/fab-modal";
@import "modules/base/fab-input";
@import "modules/base/fab-button";

View File

@ -0,0 +1,23 @@
.array-mapping-form {
.mapping-item {
margin: 1rem;
border-left: 4px solid var(--gray-soft-dark);
border-radius: 4px;
padding-left: 1em;
display: flex;
flex-direction: row;
align-items: center;
.inputs {
width: 85%;
}
.actions {
padding: 1em;
.delete-button {
background-color: #cb1117;
color: white;
}
}
}
}

View File

@ -1,25 +1,4 @@
.data-mapping-form {
.data-mapping-item {
margin: 1rem;
border-left: 4px solid var(--gray-soft-dark);
border-radius: 4px;
padding-left: 1em;
display: flex;
flex-direction: row;
align-items: center;
.inputs {
width: 85%;
}
.actions {
padding: 1em;
.delete-button {
background-color: #cb1117;
color: white;
}
}
}
.local-data,
.remote-data{
display: flex;

View File

@ -257,8 +257,20 @@ en:
authentication_type_is_required: "Authentication type is required."
data_mapping: "Data mapping"
expected_data_type: "Expected data type"
TYPE_expected: "{TYPE} expected"
input_format: "Input format"
mappings: "Mappings"
mapping_from: "From"
mapping_to: "To"
true_value: "True value"
false_value: "False value"
date_format: "Date format"
types:
integer: "integer"
string: "string"
text: "text"
date: "date"
boolean: "boolean"
#edition/creation form of an OAuth2 authentication provider
oauth2:
common_url: "Server root URL"

View File

@ -33,7 +33,7 @@ const customConfig = {
}),
isDevelopment && new (require('@pmmmwh/react-refresh-webpack-plugin'))(),
isDevelopment && new (require('eslint-webpack-plugin'))({
extensions: ['js', 'ts', 'tsx'],
extensions: ['js', 'ts', 'tsx']
})
].filter(Boolean),
module: {