Skip to content
Snippets Groups Projects
Commit 9aeef0c5 authored by Olivier Goulet's avatar Olivier Goulet
Browse files

Add interest group member logos

parent 25471a3e
No related tags found
1 merge request!1Draft: Add interest group member logos
Showing
with 731 additions and 6 deletions
/*
* Copyright (c) 2023 Eclipse Foundation, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* Contributors:
* Olivier Goulet <olivier.goulet@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
*/
import 'isomorphic-fetch';
import { getOrganizationById } from './eclipsefdn.membership';
const apiBasePath = 'https://projects.eclipse.org/api/interest-groups';
const interestGroupMapper = ig => {
return {
leads: ig.leads.map(lead => ({
fullName: lead.full_name,
...lead,
})),
participants: ig.participants.map(participant => ({
fullName: participant.full_name,
...participant,
})),
foundationDbProjectId: ig.foundationdb_project_id,
gitlab: {
projectGroup: ig.gitlab.project_group,
ignoredSubGroups: ig.gitlab.ignored_sub_groups,
},
...ig,
};
};
export const getInterestGroup = async ({ interestGroupNodeId, interestGroupId }) => {
try {
let url;
if (interestGroupId) {
url = `${apiBasePath}?project_id=foundation-internal.ig.${interestGroupId}`;
} else if (interestGroupNodeId) {
url = `${apiBasePath}/${interestGroupNodeId}`;
} else {
throw new Error('No interestGroupId or interestGroupNodeId provided to getInterestGroup');
}
const response = await fetch(url);
if (!response.ok) throw new Error(
interestGroupId
? `Could not fetch interest group for id "${interestGroupId}". Ensure that you are using the same value as project_short_id from the API`
: `Could not fetch interest group for node id "${interestGroupNodeId}"`
);
// Request without node id will return an array of one element
const data = interestGroupNodeId
? await response.json()
: (await response.json())[0];
const interestGroup = interestGroupMapper(data);
return [interestGroup, null];
} catch (error) {
return [null, error];
}
}
export const getInterestGroups = async () => {
try {
const response = await fetch(`${apiBasePath}`);
if (!response.ok) throw new Error(`Could not fetch interest groups`);
const data = await response.json();
const interestGroups = data.map(interestGroupMapper);
return [interestGroups, null];
} catch (error) {
return [null, error];
}
}
export const getInterestGroupParticipantOrganizations = async (options) => {
try {
const [interestGroup, error] = await getInterestGroup(options);
if (error) throw new Error(error);
/*
Create an array from a set of participants' organizations. The set guarantees that the
organizations have no duplicates.
*/
const participatingOrganizationIds = [
...new Set(interestGroup.participants.map(p => p.organization.id)),
];
/*
Get all participating organizations from organization id. Filter out the ones which had errors
for whatever reason.
*/
const participatingOrganizations = (
await Promise.all(
participatingOrganizationIds.map(async participantId => {
const [participant, participantError] = await getOrganizationById(participantId);
if (participantError) {
console.error(`Could not fetch participant organization from id ${participantId}`);
}
return participant;
})
)
).filter(participant => participant !== null);
return [participatingOrganizations, null];
} catch (error) {
console.error(error);
return [null, error];
}
};
/*!
* Copyright (c) 2021 Eclipse Foundation, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* Contributors:
* Christopher Guindon <chris.guindon@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
*/
import parse from 'parse-link-header';
import 'isomorphic-fetch';
const getMembers = (url = '', members = [], errHandler) => {
return new Promise((resolve, reject) =>
fetch(url)
.then((response) => {
if (response.status !== 200) {
throw `${response.status}: ${response.statusText}`;
}
response
.json()
.then((data) => {
members = members.concat(data);
const linkHeader = parse(response.headers.get('Link'));
if (linkHeader?.next) {
const { url } = linkHeader.next;
getMembers(url, members, errHandler).then(resolve).catch(reject);
} else {
members.sort((a, b) => a.name.localeCompare(b.name));
resolve(members);
}
})
.catch(reject);
})
.catch((err) => {
errHandler && errHandler(err);
reject(err);
})
);
};
export default getMembers;
/*
* Copyright (c) 2023 Eclipse Foundation, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* Contributors:
* Olivier Goulet <olivier.goulet@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
*/
import 'isomorphic-fetch';
import { convertToQueryString } from '../utils/utils';
const apiBasePath = 'https://membership.eclipse.org/api';
export const organizationMapper = organization => {
const { organization_id: organizationId, ...otherOrganizationFields } = organization;
return {
organizationId,
...otherOrganizationFields,
};
};
export const getOrganizationById = async organizationId => {
try {
const response = await fetch(`${apiBasePath}/organizations/${organizationId}`);
if (!response.ok) throw new Error(`Could not fetch organization by id ${organizationId}`);
const data = await response.json();
const organization = organizationMapper(data);
return [organization, null];
} catch (error) {
console.error(error);
return [null, error];
}
};
export const getOrganizations = async (params = {}) => {
try {
const queryString = convertToQueryString(params);
const response = await fetch(`${apiBasePath}/organizations?${queryString}`);
if (!response.ok) {
throw new Error(`Could not fetch organizations`);
}
const data = await response.json();
const organizations = data
.map(organizationMapper)
return [organizations, null];
} catch (error) {
console.error(error);
return [null, 'An unexpected error has occurred when fetching organizations'];
}
}
export const getProjectParticipatingOrganizations = async (projectId, params = {}) => {
try {
if (!projectId) {
throw new Error('No project id provided for fetching project participating organizations');
}
const queryString = convertToQueryString(params);
const response = await fetch(`${apiBasePath}/projects/${projectId}/organizations?${queryString}`);
if (!response.ok) {
throw new Error(`Could not fetch project organizations for project id "${projectId}"`);
}
const data = await response.json();
const organizations = data
.map(organizationMapper)
.sort((a, b) => a.name.localeCompare(b.name));
return [organizations, null];
} catch (error) {
console.error(error);
return [
null,
'An unexpected error has occurred when fetching participating organizations for project',
];
}
};
export const SD_IMG = 'https://www.eclipse.org/membership/images/type/strategic-members.png';
export const AP_IMG = 'https://www.eclipse.org/membership/images/type/contributing-members.png';
export const AS_IMG = 'https://www.eclipse.org/membership/images/type/associate-members.png';
/*!
* Copyright (c) 2021, 2023 Eclipse Foundation, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* Contributors:
* Christopher Guindon <chris.guindon@eclipse-foundation.org>
* Zhou Fang <zhou.fang@eclipse-foundation.org>
* Olivier Goulet <olivier.goulet@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
*/
import $ from 'jquery';
import 'jquery-match-height';
import getMembers from './api/eclipsefdn.members';
import { displayErrMsg, validateURL } from './utils/utils';
import template from './templates/explore-members/member.mustache';
import templateOnlyLogos from './templates/wg-members/wg-member-only-logos.mustache';
import templateLogoTitleWithLevels from './templates/wg-members/wg-member-logo-title-with-levels.mustache';
import templateLoading from './templates/loading-icon.mustache';
import { SD_IMG, AP_IMG, AS_IMG } from './eclipsefdn.constants.js';
import { getInterestGroupParticipantOrganizations } from './api/eclipsefdn.interest-groups';
import { organizationMapper } from './api/eclipsefdn.membership';
const SD_NAME = 'Strategic Members';
const AP_NAME = 'Contributing Members';
const AS_NAME = 'Associate Members';
const defaultOptions = {
mlWg: null,
mlLevel: null,
mlSort: null,
mlLinkMemberWebsite: null,
type: 'working-group',
id: null
};
const EclipseFdnMembersList = (async () => {
const element = document.querySelector('.interest-group-test');
if (!element) return;
element.innerHTML = templateLoading();
let membersListArray = [
{ level: SD_NAME, members: [] },
{ level: AP_NAME, members: [] },
{ level: AS_NAME, members: [] },
];
let url = 'https://membership.eclipse.org/api/organizations?pagesize=100';
const options = { ...defaultOptions, ...element.dataset };
const isRandom = options.mlSort === 'random';
const level = options.mlLevel;
const linkMemberWebsite = element.getAttribute('data-ml-link-member-website') === 'true' ? true : false;
const industryCollaborationId = options.id || options.mlWg;
if (level) {
level.split(' ').forEach((item) => (url = `${url}&levels=${item}`));
}
let membersData = [];
if (options.type === 'working-group') {
if (industryCollaborationId) {
url = `${url}&working_group=${industryCollaborationId}`;
}
// When we show members belong to an industry collaboration, no need to show EF levels
membersListArray = [{ level: '', members: [] }];
membersData = (await getMembers(url, [], (err) => displayErrMsg(element, err)))
.map(organizationMapper);
} else if (options.type === 'interest-group') {
membersListArray = [{ level: '', members: [] }];
// We assume the industry collaboration id is a Drupal node ID if it is numeric
// This widget needs to support node id and short ids for interest groups
const isNodeId = !Number.isNaN(parseInt(industryCollaborationId));
[membersData] = await getInterestGroupParticipantOrganizations({
interestGroupId: isNodeId ? undefined : industryCollaborationId,
interestGroupNodeId: isNodeId ? industryCollaborationId : undefined
});
}
const addMembersWithoutDuplicates = (levelName, memberData) => {
// When we show members belong to a wg, only 1 item exits in membersListArray
const currentLevelMembers = industryCollaborationId ? membersListArray[0] : membersListArray.find((item) => item.level === levelName);
const isDuplicatedMember = currentLevelMembers.members.find(
(item) => item.organizationId === memberData.organizationId
);
!isDuplicatedMember && currentLevelMembers.members.push(memberData);
};
membersData = membersData.map((member) => {
if (!member.name) {
// will not add members without a title/name into membersListArray to display
return member;
}
if (member.levels.find((item) => item.level?.toUpperCase() === 'SD')) {
if (!member.logos.web) {
member.logos.web = SD_IMG;
}
addMembersWithoutDuplicates(SD_NAME, member);
return member;
}
if (member.levels.find((item) => item.level?.toUpperCase() === 'AP' || item.level?.toUpperCase() === 'OHAP')) {
if (!member.logos.web) {
member.logos.web = AP_IMG;
}
addMembersWithoutDuplicates(AP_NAME, member);
return member;
}
if (member.levels.find((item) => item.level?.toUpperCase() === 'AS')) {
if (!member.logos.web) {
member.logos.web = AS_IMG;
}
addMembersWithoutDuplicates(AS_NAME, member);
return member;
}
return member;
});
if (isRandom) {
// Sort randomly
membersListArray.forEach((eachLevel) => {
let tempArray = eachLevel.members.map((item) => item);
const randomItems = [];
eachLevel.members.forEach(() => {
const randomIndex = Math.floor(Math.random() * tempArray.length);
randomItems.push(tempArray[randomIndex]);
tempArray.splice(randomIndex, 1);
});
eachLevel.members = randomItems;
});
} else {
// Sort alphabetically by default
membersListArray.forEach((eachLevel) => {
eachLevel.members.sort((a, b) => {
const preName = a.name.toUpperCase();
const nextName = b.name.toUpperCase();
if (preName < nextName) {
return -1;
}
if (preName > nextName) {
return 1;
}
return 0;
});
});
}
if (level) {
membersListArray = membersListArray.filter((list) => list.members.length !== 0);
}
const urlLinkToLogo = function () {
if (linkMemberWebsite && validateURL(this.website)) {
return this.website;
}
return `https://www.eclipse.org/membership/showMember.php?member_id=${this.organizationId}`;
};
const displayMembersByLevels = async (theTemplate) => {
if (options.type !== 'working-group') {
console.error('Only "working-group" type is supported for displaying members by level at this time');
return;
}
const allWGData = await (await fetch('https://membership.eclipse.org/api/working_groups')).json();
let currentWGLevels = allWGData.find((item) => item.alias === industryCollaborationId).levels;
// defaultLevel is for members without a valid level. So far, only occurs on IoT.
const defaultLevel = element.getAttribute('data-ml-default-level');
const specifiedLevel = element.getAttribute('data-ml-wg-level');
if (specifiedLevel) {
currentWGLevels = currentWGLevels.filter((level) =>
specifiedLevel.toLowerCase().includes(level.relation.toLowerCase())
);
}
element.innerHTML = '';
if (defaultLevel) {
currentWGLevels.push({ relation: 'default', description: defaultLevel, members: [] });
}
// categorize members into corresponding levels
currentWGLevels.forEach((level) => {
level['members'] = [];
membersListArray[0].members.forEach((member) => {
const memberLevel = member.wgpas.find((wgpa) => wgpa.working_group === industryCollaborationId)?.level;
if (memberLevel === level.relation) {
level.members.push(member);
}
if (level.relation === 'default' && memberLevel === null) {
level.members.push(member);
}
});
if (level.members.length === 0) {
return;
}
const showLevelUnderLogo = () => {
if (
!element.getAttribute('data-ml-level-under-logo') ||
element.getAttribute('data-ml-level-under-logo') === 'false'
) {
return false;
}
return level.description.replaceAll(' Member', '');
};
element.innerHTML =
element.innerHTML +
theTemplate({ item: level.members, levelDescription: level.description, urlLinkToLogo, showLevelUnderLogo });
});
};
switch (element.getAttribute('data-ml-template')) {
case 'only-logos':
element.innerHTML = templateOnlyLogos({
item: membersListArray[0].members,
urlLinkToLogo,
});
return;
case 'logo-title-with-levels':
await displayMembersByLevels(templateLogoTitleWithLevels);
$.fn.matchHeight._applyDataApi();
return;
case 'logo-with-levels':
displayMembersByLevels(templateOnlyLogos);
return;
default:
break;
}
element.innerHTML = template({
sections: membersListArray,
hostname: window.location.hostname.includes('staging.eclipse.org')
? 'https://staging.eclipse.org'
: 'https://www.eclipse.org',
});
$.fn.matchHeight._applyDataApi();
})();
export default EclipseFdnMembersList;
/*!
* Copyright (c) 2021 Eclipse Foundation, Inc.
* Copyright (c) 2021, 2023 Eclipse Foundation, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
......@@ -7,8 +7,10 @@
*
* Contributors:
* Christopher Guindon <chris.guindon@eclipse-foundation.org>
* Olivier Goulet <olivier.goulet@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
*/
import 'eclipsefdn-solstice-assets'
\ No newline at end of file
import 'eclipsefdn-solstice-assets'
import './eclipsefdn.interest-group-members-list'
\ No newline at end of file
{{#section}}
<div class="col-md-14 col-lg-17">
{{#validateURL}}
<a href="{{website}}" title="{{name}}" target="_blank">
{{/validateURL}}
{{#logos.web}}
<img src="{{logos.web}}" alt="{{name}} logo" title="{{name}}" class="img-responsive padding-bottom-25" />
{{/logos.web}}
{{^logos.web}}
<h1>{{name}}</h1>
{{/logos.web}}
{{#validateURL}}
</a>
{{/validateURL}}
<p>{{{trimDescription}}}</p>
{{#listings}}
<h2>{{name}}&apos;s Marketplace Listings</h2>
<ul>
</ul>
{{/listings}}
</div>
<div class="col-md-10 col-lg-7">
<div style="border:1px solid #eee; padding:10px" class="margin-bottom-20">
<img class="img-responsive" src={{getMemberLevelImg}} />
</div>
{{#projects.length}}
<div class="text-highlight margin-bottom-20">
<i class="fa pull-left fa-trophy orange fa-4x margin-top-10 margin-bottom-25"></i>
<h3 class="h5 fw-700">{{name}}</h3>
<p>{{name}} contributes to one or more <a href="#projects">Eclipse Projects!</a></p>
</div>
{{/projects.length}}
{{#shouldShowLinksSideBar}}
<div class="sideitem">
<h6>Links</h6>
<ul class="fa-ul">
{{#products.length}}
<li>
<i class="fa-li fa fa-chevron-circle-right orange"></i>
{{name}}&apos;s Other Products and Services:
<ul>
{{#products}}
<li><a href={{product_url}} target="_blank">{{name}}</a></li>
{{/products}}
</ul>
</li>
{{/products.length}}
{{#projects.length}}
<li>
<i class="fa-li fa fa-chevron-circle-right orange"></i>
{{name}} is an Active Contributor to the following Project(s):
<ul>
{{#projects}}
{{#active}}
<li><a href="https://projects.eclipse.org/projects/{{project_id}}" target="_blank">{{name}}</a></li>
{{/active}}
{{/projects}}
</ul>
</li>
{{/projects.length}}
</ul>
</div>
{{/shouldShowLinksSideBar}}
<div class="sideitem">
<h6>Interact</h6>
<ul class="fa-ul">
<li>
<i class="fa-li fa fa-chevron-circle-right orange"></i>
<a href="https://membership.eclipse.org/portal/org-profile">Edit This Page</a>
</li>
</ul>
</div>
</div>
{{/section}}
\ No newline at end of file
{{#sections}}
<div class="row">
<h2>{{level}}</h2>
{{#.}}
{{#members}}
<div class="col-xs-24 col-sm-12 col-md-8 margin-bottom-20 m-card">
<div class="bordered-box text-center">
<div class="box-header background-light-grey vertical-align" data-mh="m-header">
<h3 class="h4 margin-0"><a href="{{hostname}}/membership/showMember.php?member_id={{organization_id}}" title="{{name}}">{{name}}</a></h3>
</div>
<div class="box-body vertical-align" style="height: 160px">
<div class="image-container">
<a href="{{hostname}}/membership/showMember.php?member_id={{organization_id}}" title="{{name}}">
<img src="{{logos.web}}" class="img-responsive margin-auto logos" alt="{{name}} logo">
</a>
</div>
</div>
</div>
</div>
{{/members}}
{{/.}}
</div>
{{/sections}}
<div class="text-center">
<i class="fa fa-spinner fa-pulse fa-2x fa-fw margin-20"></i>
<span class="sr-only">Loading...</span>
</div>
{{#sections}}
<div class="row">
<h2>{{level}}</h2>
{{#.}}
{{#members}}
<div class="col-xs-24 col-sm-12 col-md-8 margin-bottom-20 m-card">
<div class="bordered-box text-center">
<div class="box-header background-light-grey vertical-align" data-mh="m-header">
<h3 class="h4 margin-0"><a href="{{hostname}}/membership/showMember.php?member_id={{organization_id}}" title="{{name}}">{{name}}</a></h3>
</div>
<div class="box-body vertical-align" style="height: 160px">
<div class="image-container">
<a href="{{hostname}}/membership/showMember.php?member_id={{organization_id}}" title="{{name}}">
<img src="{{logos.web}}" class="img-responsive margin-auto logos" alt="{{name}} logo">
</a>
</div>
</div>
</div>
</div>
{{/members}}
{{/.}}
</div>
{{/sections}}
<h2>{{levelDescription}}</h2>
<div class="row">
{{#item}}
<div class="col-xs-24 col-sm-12 col-md-8 margin-bottom-20 m-card">
<div class="bordered-box text-center">
<div class="box-header background-light-grey vertical-align" data-mh="m-header"">
<h3 class="h4 margin-0">
<a href="{{urlLinkToLogo}}" title="{{name}}">{{name}}</a>
</h3>
</div>
<div class="box-body vertical-align" style="height: 160px">
<div class="image-container">
<a href="{{urlLinkToLogo}}" title="{{name}}">
<img src="{{logos.web}}" class="img-responsive margin-auto logos" alt="{{name}} logo">
</a>
</div>
</div>
</div>
</div>
{{/item}}
</div>
{{#item}}
<li class="members-item flex-center flex-column">
<a target="_blank" href="{{urlLinkToLogo}}" class="flex-center">
<img alt="{{name}}" class="img-responsive" src="{{logos.web}}">
</a>
{{#showLevelUnderLogo}}
<span>
{{showLevelUnderLogo}}
</span>
{{/showLevelUnderLogo}}
</li>
{{/item}}
/*!
* Copyright (c) 2021, 2022, 2023 Eclipse Foundation, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* Contributors:
* Zhou Fang <zhou.fang@eclipse-foundation.org>
* Eric Poirier <eric.poirier@eclipse-foundation.org>
* Olivier Goulet <olivier.goulet@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
*/
export const displayErrMsg = (element, err = '', errText = 'Sorry, something went wrong, please try again later.') =>
(element.innerHTML = `<div class="alert alert-danger" role="alert"><p><strong>Error ${err}</strong></p> <p>${errText}</p></div>`);
const absUrlPattern = /^[a-zA-Z]+:\/\//;
export const validateURL = (url) => {
if (!url) return false;
const isAbsolute = url.match(absUrlPattern);
const base = isAbsolute ? undefined : window.location.href;
let isValidate;
try {
// This will fail if the url is not valid
new URL(url, base);
isValidate = true;
} catch (error) {
isValidate = false;
}
return isValidate;
};
import querystring from 'querystring';
export const convertToQueryString = params => {
// Filters out undefined params
const filteredParams = Object.fromEntries(
Object
.entries(params)
.filter(([_, v]) => v !== undefined)
);
return querystring.stringify(filteredParams);
}
export const scrollToAnchor = () => {
const elementId = location.hash.replace('#', '');
const element = document.getElementById(elementId);
element.scrollIntoView();
}
\ No newline at end of file
......@@ -2,7 +2,7 @@
<div class="margin-top-20">
<h2 class="heading-line text-center" style="color:rgb(245,147,49); font-weight: bold;">Our Members</h2>
<div class="container-fluid text-center">
<ul class="eclipsefdn-members-list list-inline margin-30 flex-center gap-50" data-ml-wg="openmobility"
data-ml-template="only-logos"></ul>
<ul class="interest-group-test list-inline margin-30 flex-center gap-50" data-type="interest-group"
data-id="openmobility" data-ml-template="only-logos"></ul>
</div>
</div>
<div id="members" class="eclipsefdn-members-list" data-ml-wg="openmobility" data-ml-template="logo-title-with-levels"></div>
<ul id="members" class="interest-group-test flex-center list-inline gap-40" data-id="openmobility" data-type="interest-group" data-ml-template="only-logos"></ul>
......@@ -23,7 +23,7 @@ h1#join-us {
color: #4c4d4e;
}
.eclipsefdn-members-list {
.eclipsefdn-members-list, .interest-group-test {
a {
max-width: 135px;
img.logos.img-responsive {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment