From faf029339dd279b4f223b2685d935c672031b435 Mon Sep 17 00:00:00 2001 From: Tyler Garner Date: Tue, 29 Aug 2023 16:32:19 -0400 Subject: [PATCH 1/3] GH-152: Add an Org Owner Card for members --- business/organization.ts | 32 ++++++++++++++++++++++++++++++++ routes/org/index.ts | 3 ++- routes/org/join.ts | 28 ++-------------------------- views/nih/mixins.pug | 30 ++++++++++++++++++++++++++++++ views/org/index.pug | 3 +++ views/org/publicView.pug | 25 +------------------------ 6 files changed, 70 insertions(+), 51 deletions(-) diff --git a/business/organization.ts b/business/organization.ts index 96792724f..b25945d04 100644 --- a/business/organization.ts +++ b/business/organization.ts @@ -846,6 +846,38 @@ export class Organization { return this.getMembers(memberOptions); } + async getOwnersCardData() { + const [linkedOrgAdmins, unlinkedOrgAdmins] = await Promise.all([ + this.getLinkedMembers({ role: OrganizationMembershipRoleQuery.Admin }), + this.getUnlinkedMembers({ role: OrganizationMembershipRoleQuery.Admin }), + ]); + + // clean up admin data for the front end + const organizationAdmins = Array.prototype + .concat(linkedOrgAdmins, unlinkedOrgAdmins) + .reduce((acc, admin) => { + const { member, link } = admin; + + // linked and unlinked admins return slightly different data structures + const login = member ? member.login : admin.login; + const avatar_url = member ? member.avatar_url : admin.avatar_url; + // fallback to corporateUsername if corporateMailAddress is not available + const mailAddress = link ? link.corporateMailAddress || link.corporateUsername : undefined; + const primaryName = link ? link.corporateDisplayName || link.corporateUsername : login; + + acc.push({ + login, + mailAddress, + avatar_url, + primaryName, + }); + + return acc; + }, []); + + return organizationAdmins; + } + async getAuditLog(options?: IGetOrganizationAuditLogOptions): Promise { options = options || {}; const operations = throwIfNotGitHubCapable(this._operations); diff --git a/routes/org/index.ts b/routes/org/index.ts index cff5edcd7..28281c3b1 100644 --- a/routes/org/index.ts +++ b/routes/org/index.ts @@ -6,7 +6,6 @@ import express, { Router } from 'express'; import asyncHandler from 'express-async-handler'; const router: Router = Router(); - import { getProviders } from '../../transitional'; import { IAggregateUserSummary } from '../../business/user/aggregate'; import { TeamJoinApprovalEntity } from '../../entities/teamJoinApproval/teamJoinApproval'; @@ -97,6 +96,7 @@ router.get( const organization = req.organization; const username = req.individualContext.getGitHubIdentity().username; const individualContext = req.individualContext; + const organizationAdmins = await organization.getOwnersCardData(); const results = { orgUser: organization.memberFromEntity(await organization.getDetails()), isMembershipPublic: await organization.checkPublicMembership(username), @@ -105,6 +105,7 @@ router.get( isSudoer: false, // if (results.isAdministrator && results.isAdministrator === true) { results.isSudoer = true; teamsMaintainedHash: null, pendingApprovals: null as TeamJoinApprovalEntity[], + organizationAdmins, }; results.organizationOverview = await individualContext.aggregations.getAggregatedOrganizationOverview( organization diff --git a/routes/org/join.ts b/routes/org/join.ts index ec1612520..30c4435d8 100644 --- a/routes/org/join.ts +++ b/routes/org/join.ts @@ -86,36 +86,12 @@ async function showOrgJoinDetails(req: ReposAppRequest) { // this implementation as close to the default org get route as possible const { individualContext, organization } = req; - const [linkedOrgAdmins, unlinkedOrgAdmins, orgDetails, organizationOverview] = await Promise.all([ - organization.getLinkedMembers({ role: OrganizationMembershipRoleQuery.Admin }), - organization.getUnlinkedMembers({ role: OrganizationMembershipRoleQuery.Admin }), + const [orgDetails, organizationOverview, organizationAdmins] = await Promise.all([ organization.getDetails(), individualContext.aggregations.getAggregatedOrganizationOverview(organization), + organization.getOwnersCardData(), ]); - // clean up admin data for the front end - const organizationAdmins = Array.prototype - .concat(linkedOrgAdmins, unlinkedOrgAdmins) - .reduce((acc, admin) => { - const { member, link } = admin; - - // linked and unlinked admins return slightly different data structures - const login = member ? member.login : admin.login; - const avatar_url = member ? member.avatar_url : admin.avatar_url; - // fallback to corporateUsername if corporateMailAddress is not available - const mailAddress = link ? link.corporateMailAddress || link.corporateUsername : undefined; - const primaryName = link ? link.corporateDisplayName || link.corporateUsername : login; - - acc.push({ - login, - mailAddress, - avatar_url, - primaryName, - }); - - return acc; - }, []); - const results = { orgUser: organization.memberFromEntity(orgDetails), orgDetails, //org details from GitHub diff --git a/views/nih/mixins.pug b/views/nih/mixins.pug index c5a084825..3c339d889 100644 --- a/views/nih/mixins.pug +++ b/views/nih/mixins.pug @@ -20,3 +20,33 @@ mixin repositoryAdminCards(admins, maxCards = 5) a(href='/people?q=' + admin.login)= admin.login - var title = admin.adminType == "Org Admin" ? organization.name + ' organization owner' : repo.name + 'direct repo owner' .label.label-info(title=title, style='margin-left: 5px; cursor: default')= admin.adminType + + +//- Renders organization owner card +//- admins: user objects with login, avatar_url, primaryName +//- maxCards: Maximum number of cards to render +mixin orgAdminCards(organizationAdmins, maxCards = 5) + ul.list-unstyled + - var admins = organizationAdmins + - var cardsToShow = Math.min(admins.length, maxCards) + - for (var i = 0; i < cardsToShow; i++) + - var admin = admins[i] + li(style='vertical-align:top;width:370px') + ul.list-inline + if admin.avatar_url + li(style='vertical-align:top;margin-top:12px;padding-right:3px'): img( + alt=admin.login, + src=admin.avatar_url + '&s=96', + style='width:36px;height:36px;') + li + ul.list-unstyled(style='margin-right:16px') + li: h5(style='margin-bottom:.3em') + a(href='/people?q=' + admin.login)= admin.primaryName + .label.label-info(title=organization.name + ' organization owner', style='margin-left: 5px; cursor: default') Owner + ul.list-inline + if admin.primaryName != admin.login + li: p(style='font-size: .8em') + a(href='https://github.com/' + admin.login, target='_new', style='color: black; text-decoration:none')= admin.login + if admin.mailAddress + li: a(href='mailto:' + admin.mailAddress, title='Send email to ' + admin.mailAddress) + != octicon('mail', 16) diff --git a/views/org/index.pug b/views/org/index.pug index aa123c23c..d086d3ab0 100644 --- a/views/org/index.pug +++ b/views/org/index.pug @@ -260,6 +260,9 @@ block content h2 | ∞  small Public + hr + h3 Owners + +orgAdminCards(accountInfo.organizationAdmins, 5) hr p diff --git a/views/org/publicView.pug b/views/org/publicView.pug index 64cafa2f1..01714fb6b 100644 --- a/views/org/publicView.pug +++ b/views/org/publicView.pug @@ -129,27 +129,4 @@ block content != octicon('alert', 17) = 'Invitation Required' p Access to this organization requires an invitation from one of the following administrators. - ul.list-unstyled - - var admins = organizationAdmins - - var adminCount = admins.length - - for (var i = 0; i < adminCount; i++) - - var admin = admins[i] - li(style='vertical-align:top;width:370px') - ul.list-inline - if admin.avatar_url - li(style='vertical-align:top;margin-top:12px;padding-right:3px'): img( - alt=admin.login, - src=admin.avatar_url + '&s=96', - style='width:36px;height:36px;') - li - ul.list-unstyled(style='margin-right:16px') - li: h5(style='margin-bottom:.3em') - a(href='/people?q=' + admin.login)= admin.primaryName - .label.label-info(title=organization.name + ' organization owner', style='margin-left: 5px; cursor: default') Owner - ul.list-inline - if admin.primaryName != admin.login - li: p(style='font-size: .8em') - a(href='https://github.com/' + admin.login, target='_new', style='color: black; text-decoration:none')= admin.login - if admin.mailAddress - li: a(href='mailto:' + admin.mailAddress, title='Send email to ' + admin.mailAddress) - != octicon('mail', 16) + +orgAdminCards(organizationAdmins, 5) From 02e1cedb02102da3d4928a79d3c77df55a317098 Mon Sep 17 00:00:00 2001 From: Tyler Garner Date: Fri, 1 Sep 2023 11:52:58 -0400 Subject: [PATCH 2/3] Show org owners as well as direct owners. --- routes/org/repos.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/org/repos.ts b/routes/org/repos.ts index 84bdd66b5..33b06df44 100644 --- a/routes/org/repos.ts +++ b/routes/org/repos.ts @@ -448,7 +448,7 @@ router.get( currentManagementChain, repo, repository, - repoAdmins: await repository.getAdmins(), + repoAdmins: await repository.getAdmins(false, false), // permissions: slicePermissionsForView(filterSystemTeams(teamsFilterType.systemTeamsExcluded, systemTeams, permissions)), // systemPermissions: slicePermissionsForView(filterSystemTeams(teamsFilterType.systemTeamsOnly, systemTeams, permissions)), // collaborators: sliceCollaboratorsForView(collaborators), From 536959c79e7909bc6907e142c8dcb8dceaecea21 Mon Sep 17 00:00:00 2001 From: Tyler Garner Date: Fri, 1 Sep 2023 12:06:04 -0400 Subject: [PATCH 3/3] Conditionally show the repository admins header. --- views/repos/repo.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/repos/repo.pug b/views/repos/repo.pug index 251a67f1e..da2e35ae1 100644 --- a/views/repos/repo.pug +++ b/views/repos/repo.pug @@ -231,8 +231,8 @@ block content hr - h4 Repository Admins if repoAdmins + h4 Repository Admins +repositoryAdminCards(repoAdmins) //- Disabling this function as it is high risk: uses GHEC enterprise admin PAT and makes repo public outside of Org owner involvement //- if repoPermissions.allowAdministration && repo.private === true