Skip to content
This repository has been archived by the owner on Dec 13, 2022. It is now read-only.

fix(UI): Fix layout for Safari and form validation #11373

Merged
merged 11 commits into from
Jul 27, 2022
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 5 additions & 16 deletions www/front_src/src/Authentication/Openid/Form/inputs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { equals, isEmpty, isNil, not, path, prop } from 'ramda';
import { equals, isEmpty, not, prop } from 'ramda';
import { FormikValues } from 'formik';

import { InputProps, InputType } from '@centreon/ui';
Expand Down Expand Up @@ -34,7 +34,7 @@ import {
labelDeleteRelation,
labelAuthorizationKey,
} from '../translatedLabels';
import { AuthenticationType, AuthorizationRule } from '../models';
import { AuthenticationType } from '../models';
import {
labelActivation,
labelAuthorizations,
Expand Down Expand Up @@ -204,6 +204,7 @@ export const inputs: Array<InputProps> = [
},
{
connectedAutocomplete: {
additionalConditionParameters: [],
endpoint: contactTemplatesEndpoint,
},
fieldName: 'contactTemplate',
Expand Down Expand Up @@ -231,6 +232,7 @@ export const inputs: Array<InputProps> = [
},
{
connectedAutocomplete: {
additionalConditionParameters: [],
endpoint: contactGroupsEndpoint,
},
fieldName: 'contactGroup',
Expand All @@ -257,6 +259,7 @@ export const inputs: Array<InputProps> = [
},
{
connectedAutocomplete: {
additionalConditionParameters: [],
endpoint: accessGroupsEndpoint,
},
fieldName: 'accessGroup',
Expand All @@ -269,20 +272,6 @@ export const inputs: Array<InputProps> = [
claimValue: '',
},
deleteLabel: labelDeleteRelation,
getRequired: ({ values, index }): boolean => {
const tableValues = prop('authorizationRules', values);

const rowValues = path<AuthorizationRule>(
['authorizationRules', index],
values,
);

return isNil(prop('contactGroup', values))
? not(isNil(rowValues))
: isNil(tableValues) ||
isEmpty(rowValues?.claimValue) ||
isNil(rowValues?.accessGroup);
},
},
group: labelAuthorizations,
label: labelDefineRelationAuthorizationValueAndAccessGroup,
Expand Down
41 changes: 2 additions & 39 deletions www/front_src/src/Authentication/Openid/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,11 +448,10 @@ describe('Openid configuration form', () => {
accessGroupsEndpoint,
labelAccessGroup,
'Access Group 2',
1,
],
])(
'updates the %p field when an option is selected from the retrieved options',
async (_, retrievedOptions, endpoint, label, value, index = 0) => {
async (_, retrievedOptions, endpoint, label, value) => {
mockGetRequestsWithNoAuthorizationConfiguration();
renderOpenidConfigurationForm();

Expand Down Expand Up @@ -489,7 +488,7 @@ describe('Openid configuration form', () => {
userEvent.click(screen.getByText(value));

await waitFor(() => {
expect(screen.getAllByLabelText(label)[index]).toHaveValue(value);
expect(screen.getAllByLabelText(label)[0]).toHaveValue(value);
});
},
);
Expand All @@ -513,40 +512,4 @@ describe('Openid configuration form', () => {
);
});
});

it('displays the "Authorization value" and "Access group" fields as required when the "Contact group" field is filled', async () => {
mockGetRequestsWithNoAuthorizationConfiguration();
mockedAxios.get.mockResolvedValueOnce({
data: retrievedContactGroups,
});

renderOpenidConfigurationForm();

await waitFor(() => {
expect(screen.getByLabelText(labelContactGroup)).toBeInTheDocument();
});

userEvent.click(screen.getByLabelText(labelContactGroup));

await waitFor(() => {
expect(mockedAxios.get).toHaveBeenCalledWith(
`${contactGroupsEndpoint}?page=1&sort_by=${encodeURIComponent(
'{"name":"ASC"}',
)}&search=${encodeURIComponent('{"$and":[]}')}`,
cancelTokenRequestParam,
);
});

await waitFor(() => {
expect(screen.getByText('Contact Group 1')).toBeInTheDocument();
});

userEvent.click(screen.getByText('Contact Group 1'));

await waitFor(() => {
expect(screen.getByLabelText(labelAuthorizationValue)).toHaveAttribute(
'required',
);
});
});
});
11 changes: 1 addition & 10 deletions www/front_src/src/Authentication/Openid/useValidationSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
labelRequired,
labelInvalidURL,
labelInvalidIPAddress,
labelAtLeastOneAuthorizationIsRequired,
} from './translatedLabels';

const IPAddressRegexp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d{1,3})?$/;
Expand All @@ -29,15 +28,7 @@ const useValidationSchema = (): Yup.SchemaOf<OpenidConfiguration> => {
return Yup.object({
authenticationType: Yup.string().required(t(labelRequired)),
authorizationEndpoint: Yup.string().nullable().required(t(labelRequired)),
authorizationRules: Yup.array()
.of(authorizationSchema)
.when('contactGroup', (contactGroup, schema) => {
return contactGroup
? schema
.min(1, t(labelAtLeastOneAuthorizationIsRequired))
.required(t(labelRequired))
: schema.nullable();
}),
authorizationRules: Yup.array().of(authorizationSchema),
autoImport: Yup.boolean().required(t(labelRequired)),
baseUrl: Yup.string()
.matches(urlRegexp, t(labelInvalidURL))
Expand Down
74 changes: 46 additions & 28 deletions www/front_src/src/Authentication/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useMemo } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';

import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { useUpdateAtom } from 'jotai/utils';
import { Responsive } from '@visx/visx';

import { Box, Container, Paper, Tab } from '@mui/material';
import { TabContext, TabList, TabPanel } from '@mui/lab';
Expand Down Expand Up @@ -90,35 +89,39 @@ const useStyles = makeStyles((theme) => ({
formContainer: {
display: 'grid',
gridTemplateColumns: '1.2fr 0.6fr',
height: '100%',
overflowY: 'auto',
justifyItems: 'center',
padding: theme.spacing(3),
},
image: {
height: '200px',
opacity: 0.5,
padding: theme.spacing(0, 5),
position: 'sticky',
top: 0,
width: '200px',
},
panel: {
height: '80%',
padding: 0,
},
paper: {
boxShadow: theme.shadows[3],
height: '100%',
},
tabList: {
boxShadow: theme.shadows[2],
},
}));

const marginBottomHeight = 88;
const scrollMargin = 8;

const Authentication = (): JSX.Element => {
const classes = useStyles();
const { t } = useTranslation();

const formContainerRef = useRef<HTMLDivElement | null>(null);

const [windowHeight, setWindowHeight] = useState(window.innerHeight);
const [clientRect, setClientRect] = useState<DOMRect | null>(null);

const appliedTab = useAtomValue(appliedTabAtom);
const { themeMode } = useAtomValue(userAtom);
const setTab = useUpdateAtom(tabAtom);
Expand All @@ -127,6 +130,23 @@ const Authentication = (): JSX.Element => {
setTab(newTab);
};

const resize = (): void => {
setWindowHeight(window.innerHeight);
};

useEffect(() => {
window.addEventListener('resize', resize);

setClientRect(formContainerRef.current?.getBoundingClientRect() ?? null);

return () => {
window.removeEventListener('resize', resize);
};
}, []);

const formContainerHeight =
windowHeight - (clientRect?.top || 0) - scrollMargin;

const tabs = useMemo(
() =>
panels.map(({ title, value }) => (
Expand All @@ -136,33 +156,31 @@ const Authentication = (): JSX.Element => {
);

const tabPanels = useMemo(
() => (
<Responsive.ParentSize>
{({ height }): Array<JSX.Element> =>
panels.map(({ Component, value, image }) => (
<TabPanel
className={classes.panel}
key={value}
style={{ height: height - marginBottomHeight }}
value={value}
>
<div className={classes.formContainer}>
<Component />
<img alt="padlock" className={classes.image} src={image} />
</div>
</TabPanel>
))
}
</Responsive.ParentSize>
),
[themeMode],
() =>
panels.map(({ Component, value, image }) => (
<TabPanel className={classes.panel} key={value} value={value}>
<Box
ref={formContainerRef}
sx={{
height: `${formContainerHeight}px`,
overflowY: 'auto',
}}
>
<div className={classes.formContainer}>
<Component />
<img alt="padlock" className={classes.image} src={image} />
</div>
</Box>
</TabPanel>
)),
[themeMode, formContainerHeight],
);

return (
<Box className={classes.box}>
<TabContext value={appliedTab}>
<Container className={classes.container}>
<Paper className={classes.paper}>
<Paper square className={classes.paper}>
<TabList
className={classes.tabList}
variant="fullWidth"
Expand Down