Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: migrate members/index to oclif/core #2665

Merged
merged 8 commits into from
Mar 1, 2024
88 changes: 88 additions & 0 deletions packages/cli/src/commands/members/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {RoleCompletion} from '@heroku-cli/command/lib/completions'
import {ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'

const _ = require('lodash')

type MemberWithStatus = Heroku.TeamMember & { status?: string }
justinwilaby marked this conversation as resolved.
Show resolved Hide resolved

const buildTableColumns = (teamInvites: Heroku.TeamInvitation[]) => {
const baseColumns = {
email: {
get: ({email}: any):string => color.cyan(email),
},
role: {
get: ({role}: any):string => color.green(role),
},
}

if (teamInvites.length > 0) {
return {
...baseColumns,
status: {
get: ({status}: any):string => color.green(status),
},
}
}

return baseColumns
}

export default class MembersIndex extends Command {
static topic = 'members';
static description = 'list members of a team';
static flags = {
role: flags.string({char: 'r', description: 'filter by role', completion: RoleCompletion}),
pending: flags.boolean({description: 'filter by pending team invitations'}),
json: flags.boolean({description: 'output in json format'}),
team: flags.team({required: true}),
};

public async run(): Promise<void> {
const {flags} = await this.parse(MembersIndex)
const {role, pending, json, team} = flags
const {body: teamInfo} = await this.heroku.get<Heroku.Team>(`/teams/${team}`)
let teamInvites: Heroku.TeamInvitation[] = []
if (teamInfo.type === 'team') {
const {body: orgFeatures} = await this.heroku.get<Heroku.TeamFeature[]>(`/teams/${team}/features`)
if (orgFeatures.find((feature => feature.name === 'team-invite-acceptance' && feature.enabled))) {
const invitesResponse = await this.heroku.get<Heroku.TeamInvitation[]>(
`/teams/${team}/invitations`,
{headers: {
Accept: 'application/vnd.heroku+json; version=3.team-invitations',
},
})
teamInvites = _.map(invitesResponse.body, function (invite: Heroku.TeamInvitation) {
justinwilaby marked this conversation as resolved.
Show resolved Hide resolved
return {email: invite.user?.email, role: invite.role, status: 'pending'}
})
}
}

let {body: members} = await this.heroku.get<MemberWithStatus[]>(`/teams/${team}/members`)
// Set status '' to all existing members
_.map(members, (member: MemberWithStatus) => {
justinwilaby marked this conversation as resolved.
Show resolved Hide resolved
member.status = ''
})
members = _.sortBy(_.union(members, teamInvites), 'email')
justinwilaby marked this conversation as resolved.
Show resolved Hide resolved
if (role)
members = members.filter(m => m.role === role)
if (pending)
members = members.filter(m => m.status === 'pending')
if (json) {
ux.log(JSON.stringify(members, null, 3))
} else if (members.length === 0) {
let msg = `No members in ${color.magenta(team || '')}`
if (role)
msg += ` with role ${color.green(role)}`
ux.log(msg)
} else {
const tableColumns = buildTableColumns(teamInvites)
ux.table(
members,
tableColumns,
)
}
}
}
140 changes: 140 additions & 0 deletions packages/cli/test/unit/commands/members/index.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {stdout, stderr} from 'stdout-stderr'
import {expect} from 'chai'
import * as nock from 'nock'
import Cmd from '../../../../src/commands/members'
import runCommand from '../../../helpers/runCommand'
import {
teamInfo,
teamInvites,
teamFeatures,
teamMembers,
} from '../../../helpers/stubs/get'

describe('heroku members', () => {
afterEach(() => nock.cleanAll())
let apiGetOrgMembers: nock.Scope
const adminTeamMember = {email: 'admin@heroku.com', role: 'admin', user: {email: 'admin@heroku.com'}}
const collaboratorTeamMember = {email: 'collab@heroku.com', role: 'collaborator', user: {email: 'collab@heroku.com'}}
const memberTeamMember = {email: 'member@heroku.com', role: 'member', user: {email: 'member@heroku.com'}}
context('when it is an Enterprise team', () => {
beforeEach(() => {
teamInfo('enterprise')
})
it('shows there are not team members if it is an orphan team', () => {
apiGetOrgMembers = teamMembers([])
return runCommand(Cmd, [
'--team',
'myteam',
])
.then(() => expect('No members in myteam\n').to.eq(stdout.output))
.then(() => expect('').to.eq(stderr.output))
.then(() => apiGetOrgMembers.done())
})
it('shows all the team members', () => {
apiGetOrgMembers = teamMembers([adminTeamMember, collaboratorTeamMember])
return runCommand(Cmd, [
'--team',
'myteam',
])
.then(() => expect(stdout.output).to.contain('admin@heroku.com admin \n collab@heroku.com collaborator'))
.then(() => expect('').to.eq(stderr.output))
.then(() => apiGetOrgMembers.done())
})
it('filters members by role', () => {
apiGetOrgMembers = teamMembers([adminTeamMember, memberTeamMember])
return runCommand(Cmd, [
'--team',
'myteam',
'--role',
'member',
])
.then(() => expect(stdout.output).to.contain('member@heroku.com member'))
.then(() => expect('').to.eq(stderr.output))
.then(() => apiGetOrgMembers.done())
})
it("shows the right message when filter doesn't return results", () => {
apiGetOrgMembers = teamMembers([adminTeamMember, memberTeamMember])
return runCommand(Cmd, [
'--team',
'myteam',
'--role',
'collaborator',
])
.then(() => expect('No members in myteam with role collaborator\n').to.eq(stdout.output))
.then(() => expect('').to.eq(stderr.output))
.then(() => apiGetOrgMembers.done())
})
it('filters members by role', () => {
apiGetOrgMembers = teamMembers([adminTeamMember, memberTeamMember])
return runCommand(Cmd, [
'--team',
'myteam',
'--role',
'member',
])
.then(() => expect(stdout.output).to.contain('member@heroku.com member'))
.then(() => expect('').to.eq(stderr.output))
.then(() => apiGetOrgMembers.done())
})
})
context('when it is a team', () => {
beforeEach(() => {
teamInfo('team')
})
context('without the feature flag team-invite-acceptance', () => {
beforeEach(() => {
teamFeatures([])
})
it('does not show the status column', () => {
apiGetOrgMembers = teamMembers([adminTeamMember, memberTeamMember])
return runCommand(Cmd, [
'--team',
'myteam',
])
.then(() => expect(stdout.output).to.not.contain('Status'))
.then(() => apiGetOrgMembers.done())
})
})
context('with the feature flag team-invite-acceptance', () => {
beforeEach(() => {
teamFeatures([{name: 'team-invite-acceptance', enabled: true}])
})
it('shows all members including those with pending invites', () => {
const apiGetTeamInvites = teamInvites()
apiGetOrgMembers = teamMembers([adminTeamMember, collaboratorTeamMember])
return runCommand(Cmd, [
'--team',
'myteam',
])
.then(() => expect(stdout.output).to.contain('admin@heroku.com admin \n collab@heroku.com collaborator \n invited-user@mail.com admin pending'))
.then(() => expect('').to.eq(stderr.output))
.then(() => apiGetTeamInvites.done())
.then(() => apiGetOrgMembers.done())
})
it('does not show the Status column when there are no pending invites', () => {
const apiGetTeamInvites = teamInvites([])
apiGetOrgMembers = teamMembers([adminTeamMember, collaboratorTeamMember])
return runCommand(Cmd, [
'--team',
'myteam',
])
.then(() => expect(stdout.output).to.not.contain('Status'))
.then(() => apiGetOrgMembers.done())
.then(() => apiGetTeamInvites.done())
})
it('filters members by pending invites', () => {
const apiGetTeamInvites = teamInvites()
apiGetOrgMembers = teamMembers([adminTeamMember, collaboratorTeamMember])
return runCommand(Cmd, [
'--team',
'myteam',
'--pending',
])
.then(() => expect(stdout.output).to.contain('invited-user@mail.com admin pending'))
.then(() => expect('').to.eq(stderr.output))
.then(() => apiGetTeamInvites.done())
.then(() => apiGetOrgMembers.done())
})
})
})
})
69 changes: 0 additions & 69 deletions packages/orgs-v5/commands/members/index.js

This file was deleted.

1 change: 0 additions & 1 deletion packages/orgs-v5/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ exports.commands = flatten([
require('./commands/apps/lock'),
require('./commands/apps/transfer'),
require('./commands/apps/unlock'),
require('./commands/members'),
require('./commands/members/add'),
require('./commands/members/set'),
require('./commands/members/remove'),
Expand Down
Loading
Loading