Commit e5d14417 authored by Martin Lowe's avatar Martin Lowe 🇨🇦
Browse files

Merge branch 'dev' into 'master'

Merge development into master for 1.3.2 release

See merge request !460
parents 619677f7 be309639
Pipeline #1727 passed with stage
in 0 seconds
......@@ -47,7 +47,7 @@ services:
- keycloak
- foundationdb
foundationdb:
image: eclipsefdn/foundationdb-api:staging-d8e9371-9
image: eclipsefdn/foundationdb-api:staging-10b33ae-11
ports:
- '8095:8095'
environment:
......
......@@ -134,6 +134,13 @@
<version>${openhtml.version}</version>
</dependency>
<!-- Image rescaling - better results than vanilla -->
<dependency>
<groupId>org.imgscalr</groupId>
<artifactId>imgscalr-lib</artifactId>
<version>4.2</version>
</dependency>
<!-- Annotation preprocessors - reduce all of the boiler plate -->
<dependency>
<groupId>com.google.auto.value</groupId>
......@@ -261,7 +268,7 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<skipTests>false</skipTests>
<skipTests>false</skipTests>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
......
......@@ -153,19 +153,27 @@ public class OrganizationResource extends AbstractRESTResource {
@RolesAllowed({ CR, DE, CRA, MA })
@Path("{orgID:\\d+}")
public Response update(@PathParam("orgID") String organizationID, OrganizationInfoUpdateRequest updateRequest) {
if (updateRequest.getDescription().length() > 700) {
return new org.eclipsefoundation.core.model.Error(400, "Organization description should not be over 700 characters").asResponse();
}
// get ref and update the object
OrganizationInformation infoRef = eclipseDBDao.getReference(Integer.valueOf(organizationID),
OrganizationInformation.class);
if (infoRef == null) {
return Response.status(404).build();
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add(DefaultUrlParameterNames.ID.getName(), organizationID);
List<OrganizationInformation> infoRefs = eclipseDBDao
.get(new RDBMSQuery<>(wrap, filters.get(OrganizationInformation.class), params));
// if ref doesn't exist, create one
OrganizationInformation infoRef;
if (infoRefs.isEmpty()) {
infoRef = new OrganizationInformation();
infoRef.setOrganizationID(Integer.valueOf(organizationID));
} else {
infoRef = infoRefs.get(0);
}
infoRef.setCompanyUrl(updateRequest.getCompanyUrl());
infoRef.setShortDescription(updateRequest.getDescription());
infoRef.setLongDescription(updateRequest.getDescription());
// create the param map and update the org info
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add(DefaultUrlParameterNames.ID.getName(), organizationID);
// update the org info
List<OrganizationInformation> updatedOrg = eclipseDBDao.add(
new RDBMSQuery<>(wrap, filters.get(OrganizationInformation.class), params), Arrays.asList(infoRef));
if (updatedOrg.isEmpty()) {
......
......@@ -73,7 +73,7 @@ public interface ImageStoreService {
String webRoot();
@WithDefault("65000")
@WithDefault("1000000")
long maxSizeInBytes();
@WithDefault("")
......@@ -94,6 +94,9 @@ public interface ImageStoreService {
@WithDefault("true")
boolean enabled();
@WithDefault("200")
int maxDimension();
@WithDefault("0.80f")
@Max(1)
float factor();
......
......@@ -39,10 +39,12 @@ import javax.imageio.stream.ImageOutputStream;
import javax.inject.Inject;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.eclipsefoundation.core.model.RequestWrapper;
import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
import org.eclipsefoundation.eclipsedb.dao.EclipseDBPersistenceDAO;
import org.eclipsefoundation.eclipsedb.dto.OrganizationInformation;
import org.eclipsefoundation.persistence.model.RDBMSQuery;
......@@ -50,6 +52,8 @@ import org.eclipsefoundation.persistence.service.FilterService;
import org.eclipsefoundation.react.helper.ImageFileHelper;
import org.eclipsefoundation.react.namespace.ImageStoreFormat;
import org.eclipsefoundation.react.service.ImageStoreService;
import org.imgscalr.Scalr;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -142,33 +146,24 @@ public class DefaultImageStoreService implements ImageStoreService {
Path p = imageStoreRoot.resolve(ImageFileHelper.getFileNameWithExtension(fileName, format, mimeType));
BasicFileAttributeView attrView = Files.getFileAttributeView(p, BasicFileAttributeView.class);
try {
// for images bigger than the current max that don't already exist, refuse to write them
byte[] bytes = imageBytes.get();
if (bytes == null) {
LOGGER.warn("Could not generate image file with name {} and format {}, no image passed", fileName,
format);
// cannot provide image, return null
return null;
} else if (bytes.length > config.maxSizeInBytes() && (!Files.exists(p)
|| !approximatelyMatch((long) bytes.length, attrView.readAttributes().size(), 1000))) {
}
// compress image and then compare max size
byte[] compressedImage = compress(bytes, mimeType);
if (compressedImage.length > config.maxSizeInBytes() && (!Files.exists(p)
|| !approximatelyMatch((long) compressedImage.length, attrView.readAttributes().size(), 1000))) {
throw new BadRequestException(
"Passed image is larger than allowed size of '" + config.maxSizeInBytes() + "' bytes");
}
// if enabled, update the EclipseDB on logo update when image name is numeric
// max size check is related to max blob size of 64kb
// TODO remove once the Drupal API references this API as this will no longer be needed then
if (config.persistToDb() && format.isPresent()
&& ImageStoreFormat.ImageStoreFormats.WEB.equals(format.get()) && StringUtils.isNumeric(fileName)) {
// get a ref to the given organization information object
OrganizationInformation oi = dao.getReference(Integer.valueOf(fileName), OrganizationInformation.class);
// as long as this org exists, update the logo
if (oi != null) {
oi.setLargeLogo(bytes);
oi.setSmallLogo(bytes);
oi.setLargeMime(mimeType);
oi.setSmallMime(mimeType);
dao.add(new RDBMSQuery<>(new RequestWrapper(), filters.get(OrganizationInformation.class)), Arrays.asList(oi));
}
}
handleDBPersist(fileName, compressedImage, mimeType, format);
// write will create and overwrite file by default if it exists
return getWebUrl(Files.write(p, compress(bytes, mimeType)));
......@@ -214,6 +209,49 @@ public class DefaultImageStoreService implements ImageStoreService {
});
}
/**
* Handles persistence to the DB layer for images. Includes checks for size, format, and format before persisting to
* the database.
*
* @param fileName name of the file to be persisted
* @param compressedImage the compressed image bytes
* @param mimeType the mime type of the image
* @param format the format of the file
*/
private void handleDBPersist(String fileName, byte[] compressedImage, String mimeType,
Optional<ImageStoreFormat> format) {
if (config.persistToDb() && format.isPresent() && ImageStoreFormat.ImageStoreFormats.WEB.equals(format.get())
&& StringUtils.isNumeric(fileName)) {
if (compressedImage.length < 65535) {
LOGGER.warn("Cannot persist image with name {}, size of compressed image is over max size of table",
fileName);
} else {
// get a ref to the given organization information object
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add(DefaultUrlParameterNames.ID.getName(), fileName);
List<OrganizationInformation> infoRefs = dao
.get(new RDBMSQuery<>(new RequestWrapper(), filters.get(OrganizationInformation.class), params));
// if ref doesn't exist, create one
OrganizationInformation oi;
if (infoRefs.isEmpty()) {
oi = new OrganizationInformation();
oi.setOrganizationID(Integer.valueOf(fileName));
oi.setCompanyUrl("");
} else {
oi = infoRefs.get(0);
}
// as long as this org exists, update the logo
oi.setLargeLogo(compressedImage);
oi.setSmallLogo(compressedImage);
oi.setLargeMime(mimeType);
oi.setSmallMime(mimeType);
dao.add(new RDBMSQuery<>(new RequestWrapper(), filters.get(OrganizationInformation.class)),
Arrays.asList(oi));
}
}
}
/**
* Fuzzy match used to check byte size of files. Used just in case there is some minor changes to the data done via
* metadata or the like.
......@@ -256,7 +294,7 @@ public class DefaultImageStoreService implements ImageStoreService {
ImageOutputStream ios = ImageIO.createImageOutputStream(os)) {
// read in the image bytes for image io
// Note: if this fails due to an zlib exception, the image is most likely corrupt somehow
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
BufferedImage image = rescale(ImageIO.read(new ByteArrayInputStream(imageBytes)));
// get the image writer for the current mime type
Iterator<ImageWriter> writers = ImageIO
......@@ -284,4 +322,21 @@ public class DefaultImageStoreService implements ImageStoreService {
}
}
}
/**
* Using Scalr library, uses quality-focused resizing algorithms to shrink images larger than the configured maximum
* dimension.
*
* @param original the original image to resize
* @return the resized image if larger than max dimensions, or original image
*/
private BufferedImage rescale(BufferedImage original) {
// check if we need to rescale the image to save cycles
if (original.getHeight() < config.compression().maxDimension()
&& original.getWidth() < config.compression().maxDimension()) {
return original;
}
// use Scalr to maintain ratio and resize image
return Scalr.resize(original, Scalr.Method.QUALITY, config.compression().maxDimension());
}
}
......@@ -45,7 +45,7 @@
},
"asciidoc": {
"alias": "asciidoc",
"title": " AsciiDoc® Working Group",
"title": "AsciiDoc® Working Group",
"status": "active",
"logo": "https://www.eclipse.org/org/workinggroups/assets/images/wg_asciidoc.svg",
"description": "The AsciiDoc® Working Group drives the standardization, adoption, and evolution of AsciiDoc. This group encourages and shapes the open, collaborative development of the AsciiDoc language and its processors.",
......@@ -747,7 +747,7 @@
}
]
},
"openhw-europe":{
"openhw-europe": {
"alias": "openhw-europe",
"title": "OpenHW Europe Working Group",
"status": "active",
......@@ -778,7 +778,41 @@
"description": "Guest Member"
}
]
},
"aice": {
"alias": "aice",
"title": "Eclipse AI, Cloud & Edge (AICE) Working Group",
"status": "proposed",
"logo": "https://www.eclipse.org/org/workinggroups/assets/images/wg-aice.svg",
"description": "The Eclipse AI, Cloud & Edge (AICE) Working Group will manage and operate an open lab (the “AICE OpenLab”) that will provide a set of resources to promote the advancement, implementation, and verification of open source software for AI, Cloud, and Edge computing.",
"resources": {
"charter": "https://www.eclipse.org/org/workinggroups/aice-charter.php",
"participation_agreements": {
"individual": null,
"organization": {
"document_id": "a8bc59bfcd17d5a42f1e",
"pdf": "https://www.eclipse.org/org/workinggroups/wgpa/aice-working-group-participation-agreement.pdf"
}
},
"website": "https://aice.eclipse.org/",
"members": "",
"sponsorship": "",
"contact_form": "https://accounts.eclipse.org/contact/membership"
},
"levels": [
{
"relation": "WGSD",
"description": "Strategic Member"
},
{
"relation": "WGAPS",
"description": "Participant Member"
},
{
"relation": "WGSAP",
"description": "Guest Member"
}
]
}
}
}
}
\ No newline at end of file
......@@ -19,7 +19,7 @@
"chart.js": "^3.5.0",
"concurrently": "^6.1.0",
"country-list": "^2.2.0",
"eclipsefdn-solstice-assets": "0.0.162",
"eclipsefdn-solstice-assets": "0.0.180",
"formik": "^2.2.6",
"less-watch-compiler": "^1.15.1",
"material-ui-dropzone": "^3.5.0",
......
......@@ -4,7 +4,7 @@
"id": "402880987649273c0176492a6c6200fd",
"form_id": "form_1",
"participation_level": "AsciiDoc_level_a",
"working_group_id": "ascii_doc",
"working_group": "ascii_doc",
"contact": {
"id": "111",
"form_id": "form_1",
......@@ -20,7 +20,7 @@
"id": "402880987649273c0176492a6c6300fe",
"form_id": "form_1",
"participation_level": "OpenGENESIS_level_c",
"working_group_id": "opengenesis",
"working_group": "opengenesis",
"contact": {
"id": "777",
"form_id": "form_1",
......
......@@ -19,5 +19,12 @@
"last_name": "Test",
"email": "873c6fef@ec516ea0.com",
"relations": ["DE", "MA"]
},
{
"id": "f81d34323",
"first_name": "Test",
"last_name": "Test",
"email": "dfesdfe@ec516ea0.com",
"relations": ["DE", "MA"]
}
]
......@@ -16,13 +16,13 @@ import {
getCurrentMode,
LOGIN_FROM_KEY,
MODE_REACT_ONLY,
ORIGINAL_PATH_KEY,
ROUTE_SIGN_IN,
} from './Constants/Constants';
import PortalContext from './Context/PortalContext';
import GlobalContext from './Context/GlobalContext';
import Loading from './components/UIComponents/Loading/Loading';
import { isProd } from './Utils/formFunctionHelpers';
import PortalLogin from './components/Portal/Login/PortalLogin';
import TopSlideMsg from './components/UIComponents/Notifications/TopSlideMsg';
const theme = createMuiTheme({
......@@ -158,7 +158,7 @@ const App = () => {
<Switch>
<Route exact path="/">
{localStorage.getItem(LOGIN_FROM_KEY) === 'Portal' ? (
<Redirect to="/portal" />
<Redirect to={sessionStorage.getItem(ORIGINAL_PATH_KEY) || '/portal'} />
) : (
<Redirect to="/application" />
)}
......@@ -184,22 +184,12 @@ const App = () => {
</AppTemplate>
</Route>
<Route path="/portal/login">
{/* Only show Portal when the user has a valid relation under his/her org */}
{currentUser?.relation?.length > 0 && <Redirect to="/portal" />}
<PortalLogin isFetchingUser={isFetchingUser} setIsFetchingUser={setIsFetchingUser} />
</Route>
<Route path="/portal">
{currentUser?.relation?.length ? (
<PortalContext.Provider value={PortalContextValue}>
<BrowserRouter hashType="noslash">
<Portal />
</BrowserRouter>
</PortalContext.Provider>
) : (
<Redirect to="/portal/login" />
)}
<PortalContext.Provider value={PortalContextValue}>
<BrowserRouter hashType="noslash">
<Portal isFetchingUser={isFetchingUser} setIsFetchingUser={setIsFetchingUser} />
</BrowserRouter>
</PortalContext.Provider>
</Route>
{/* Redirect user to 404 page for all the unknown pathnames/urls */}
......
......@@ -21,6 +21,7 @@ export const api_prefix = () => {
};
export const LOGIN_FROM_KEY = 'logInFrom';
export const ORIGINAL_PATH_KEY = 'originalPath';
export const API_PREFIX_FORM = api_prefix() + '/form';
export const API_FORM_PARAM = '?sort=dateCreated&order=desc';
......
......@@ -168,7 +168,7 @@ export function matchWorkingGroupFields(existingworkingGroupData, workingGroupsO
var res = [];
// Array
existingworkingGroupData.forEach((item) => {
let wg = workingGroupsOptions?.find((el) => el.label === item?.working_group_id);
let wg = workingGroupsOptions?.find((el) => el.label === item?.working_group);
const basicRepInfo = {
firstName: item?.contact?.first_name || '',
lastName: item?.contact?.last_name || '',
......@@ -180,7 +180,7 @@ export function matchWorkingGroupFields(existingworkingGroupData, workingGroupsO
workingGroup:
{
label: wg?.label,
value: item?.working_group_id,
value: item?.working_group,
participation_levels: wg?.participation_levels,
} || '',
participationLevel: item?.participation_level || '',
......@@ -291,7 +291,7 @@ export function matchWGFieldsToBackend(eachWorkingGroupData, formId) {
return {
id: eachWorkingGroupData?.id,
working_group_id: eachWorkingGroupData?.workingGroup.value,
working_group: eachWorkingGroupData?.workingGroup.value,
participation_level: eachWorkingGroupData?.participationLevel,
effective_date: theDate.toISOString().replace(/.\d+Z$/g, 'Z'),
contact: {
......@@ -737,13 +737,8 @@ export const focusOnInvalidField = () => {
};
export const fetchWrapper = (url, method, callbackFunc, dataBody, errCallbackFunc) => {
const shouldExcludeCSRF =
url.includes('https://newsroom.eclipse.org/api/resources') ||
url.includes('https://api.eclipse.org/public/member/') ||
url.includes('https://projects.eclipse.org/api/projects') ||
url.includes('https://api.eclipse.org/cbi/sponsorships');
let requestHeader = shouldExcludeCSRF ? FETCH_HEADER_WITHOUT_CSRF : FETCH_HEADER;
const shouldIncludeCSRF = url[0] === '/';
let requestHeader = shouldIncludeCSRF ? FETCH_HEADER : FETCH_HEADER_WITHOUT_CSRF;
if (url.includes('/logos') && method === 'POST') {
requestHeader = { 'x-csrf-token': FETCH_HEADER['x-csrf-token'] };
......@@ -757,7 +752,7 @@ export const fetchWrapper = (url, method, callbackFunc, dataBody, errCallbackFun
.then((res) => {
if (res.ok) {
// DELETE and 204 won't return response data, so don't do json()
return method === 'DELETE' || method === 'POST' || res.status === 204 ? res : res.json();
return method === 'DELETE' || method === 'POST' || res.status === 204 ? res : res.json();
}
throw res.status;
})
......@@ -773,8 +768,7 @@ export const fetchWrapper = (url, method, callbackFunc, dataBody, errCallbackFun
export const fetchWrapperPagination = async (url, i, callbackFunc) => {
let data = [];
const shouldIncludeCSRF = url.includes('/contacts');
const shouldIncludeCSRF = url[0] === '/';
const requestHeader = shouldIncludeCSRF ? FETCH_HEADER : FETCH_HEADER_WITHOUT_CSRF;
const getData = async () => {
......
......@@ -34,6 +34,7 @@ import { fetchWrapper, fetchWrapperPagination, isProd } from '../../../Utils/for
import GlobalContext from '../../../Context/GlobalContext';
import { checkPermission } from '../../../Utils/portalFunctionHelpers';
import HelpIcon from '@material-ui/icons/Help';
import NoAccess from '../../ErrorPages/NoAccess';
const isReactOnlyMode = getCurrentMode() === MODE_REACT_ONLY;
......@@ -492,7 +493,9 @@ export default function ContactManagement() {
saveContacts(contactData);
}, [contactData, allRelations]);
return (
return !checkPermission(PERMISSIONS_BASED_ON_ROLES.accessContacts, currentUser?.relation) ? (
<NoAccess />
) : (
<>
<RecentActorsIcon className={classes.headerIcon} />
<Typography className={classes.pageTitle} variant="h4" component="h1">
......
import {
Avatar,
Button,
Card,
CardContent,
CardMedia,
CircularProgress,
Container,
......@@ -20,8 +20,10 @@ import { useContext, useEffect, useState } from 'react';
import NoteIcon from '@material-ui/icons/Note';
import GroupIcon from '@material-ui/icons/Group';
import EmailIcon from '@material-ui/icons/Email';
import AssignmentIcon from '@material-ui/icons/Assignment';
import { OrgRep } from '../../../Interfaces/portal_interface';
import PortalContext from '../../../Context/PortalContext';
import ModalWindow from '../../UIComponents/Notifications/ModalWindow';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
......@@ -59,10 +61,12 @@ const useStyles = makeStyles((theme: Theme) =>
margin: theme.spacing(0, 'auto'),
},
companyRepCard: {
position: 'relative',
backgroundColor: '#D0D0D0',
padding: theme.spacing(2, 0, 3.5),
},
repPrimary: {
fontSize: 20,
fontSize: 18,
textAlign: 'center',
},
repSecondary: {
......@@ -87,7 +91,7 @@ const useStyles = makeStyles((theme: Theme) =>
width: '100%',
},
contentItemCtn: {
padding: theme.spacing(2, 0),
padding: theme.spacing(1.5, 0),
},
contentAvatar: {
width: 35,
......@@ -104,6 +108,19 @@ const useStyles = makeStyles((theme: Theme) =>
contentItemTextSub: {
color: 'hsl(0, 0%, 75%)',
},
viewAllBtnCtn: {
position: 'absolute',
bottom: 15,
left: 15,
right: 15,
width: '100%',
display: 'flex',
justifyContent: 'end',
alignItems: 'end',
},
viewAllBtn: {
padding: theme.spacing(0.5, 1.5),
},
})
);
......@@ -112,6 +129,7 @@ export default function DashboardIntro(props: { orgRepData: Array<OrgRep> | null
const { orgRepData } = props;
const { orgInfo } = useContext(PortalContext);
const [orgIntro, setOrgIntro] = useState({ imageURL: '', name: '', website: '' });
const [open, setOpen] = useState(false);
const renderOrgRep = orgRepData?.map((rep, index) => (
<ListItem key={index}>
......@@ -155,9 +173,12 @@ export default function DashboardIntro(props: { orgRepData: Array<OrgRep> | null
</Card>
<Card className={classNames(classes.card, classes.companyRepCard)}>
<CardContent>
<List>{orgRepData ? renderOrgRep : <CircularProgress />}</List>
</CardContent>
<List>{orgRepData ? renderOrgRep?.slice(0, 3) : <CircularProgress />}</List>
<Container className={classes.viewAllBtnCtn}>
<Button className={classes.viewAllBtn} onClick={() => setOpen(true)}>
View more
</Button>
</Container>
</Card>
<Card className={classNames(classes.card, classes.companyContentCard)}>
......@@ -209,8 +230,35 @@ export default function DashboardIntro(props: { orgRepData: Array<OrgRep> | null
</Avatar>
<Container className={classes.contentItemText}>Contact Us</Container>
</ListItem>
<Divider className={classes.divider} />
<ListItem
button
component="a"
href="https://www.eclipse.org/org/documents/"
target="_blank"
rel="noopener"
className={classes.contentItemCtn}
>
<Avatar className={classes.contentAvatar}>
<AssignmentIcon />
</Avatar>
<Container className={classes.contentItemText}>Governance Documents</Container>
</ListItem>
</List>