diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 6e45a5f4e5..d82673beef 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -28,6 +28,7 @@ import { PullRequestOptions, RepoBuild, Team, + TeamMember, } from '@backstage/plugin-azure-devops-common'; import { GitPullRequest, @@ -40,9 +41,9 @@ import { getArtifactId, } from '../utils'; +import { TeamMember as AdoTeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; import { Logger } from 'winston'; import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces'; -import { TeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; import { WebApi } from 'azure-devops-node-api'; import { WebApiTeam } from 'azure-devops-node-api/interfaces/CoreInterfaces'; @@ -228,22 +229,19 @@ export class AzureDevOpsApi { const client = await this.webApi.getCoreApi(); const webApiTeams: WebApiTeam[] = await client.getAllTeams(); - const teams: Team[] = await Promise.all( - webApiTeams.map(async team => ({ - id: team.id, - name: team.name, - memberIds: await this.getTeamMemberIds(team), - })), - ); + const teams: Team[] = webApiTeams.map(team => ({ + id: team.id, + name: team.name, + projectId: team.projectId, + projectName: team.projectName, + })); return teams.sort((a, b) => a.name && b.name ? a.name.localeCompare(b.name) : 0, ); } - private async getTeamMemberIds( - team: WebApiTeam, - ): Promise { + public async getTeamMembers(team: Team): Promise { this.logger?.debug(`Getting team member ids for team '${team.name}'.`); if (!team.projectId || !team.id) { @@ -252,15 +250,17 @@ export class AzureDevOpsApi { const client = await this.webApi.getCoreApi(); - const teamMembers: TeamMember[] = + const teamMembers: AdoTeamMember[] = await client.getTeamMembersWithExtendedProperties( team.projectId, team.id, ); - return teamMembers - .map(teamMember => teamMember.identity?.id) - .filter((id): id is string => Boolean(id)); + return teamMembers.map(teamMember => ({ + id: teamMember.identity?.id, + displayName: teamMember.identity?.displayName, + uniqueName: teamMember.identity?.uniqueName, + })); } public async getBuildDefinitions( diff --git a/plugins/azure-devops-backend/src/api/PullRequestsDashboardProvider.ts b/plugins/azure-devops-backend/src/api/PullRequestsDashboardProvider.ts new file mode 100644 index 0000000000..9bb53637f7 --- /dev/null +++ b/plugins/azure-devops-backend/src/api/PullRequestsDashboardProvider.ts @@ -0,0 +1,129 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DashboardPullRequest, + PullRequestOptions, + Team, + TeamMember, +} from '@backstage/plugin-azure-devops-common'; + +import { AzureDevOpsApi } from './AzureDevOpsApi'; +import { Logger } from 'winston'; + +export class PullRequestsDashboardProvider { + private teams = new Map(); + + private teamMembers = new Map(); + + private constructor( + private readonly logger: Logger, + private readonly azureDevOpsApi: AzureDevOpsApi, + ) {} + + public static async create( + logger: Logger, + azureDevOpsApi: AzureDevOpsApi, + ): Promise { + const provider = new PullRequestsDashboardProvider(logger, azureDevOpsApi); + await provider.readTeams(); + return provider; + } + + public async readTeams(): Promise { + this.logger.info('Reading teams.'); + + let teams = await this.azureDevOpsApi.getAllTeams(); + + // This is used to filter out the default Azure Devops project teams. + teams = teams.filter(team => + team.name && team.projectName + ? team.name !== `${team.projectName} Team` + : true, + ); + + this.teams = new Map(); + this.teamMembers = new Map(); + + await Promise.all( + teams.map(async team => { + const teamId = team.id; + + if (teamId) { + const teamMembers = await this.azureDevOpsApi.getTeamMembers(team); + + if (teamMembers) { + team.members = teamMembers.reduce((arr, teamMember) => { + const teamMemberId = teamMember.id; + + if (teamMemberId) { + arr.push(teamMemberId); + const memberOf = [ + ...(this.teamMembers.get(teamMemberId)?.memberOf ?? []), + teamId, + ]; + this.teamMembers.set(teamMemberId, { ...teamMember, memberOf }); + } + + return arr; + }, [] as string[]); + + this.teams.set(teamId, team); + } + } + }), + ); + } + + public async getDashboardPullRequests( + projectName: string, + options: PullRequestOptions, + ): Promise { + const dashboardPullRequests = + await this.azureDevOpsApi.getDashboardPullRequests(projectName, options); + + return dashboardPullRequests.map(pr => { + if (pr.createdBy?.id) { + const teamIds = this.teamMembers.get(pr.createdBy.id)?.memberOf; + pr.createdBy.teamIds = teamIds; + pr.createdBy.teamNames = teamIds?.map( + teamId => this.teams.get(teamId)?.name ?? '', + ); + } + + return pr; + }); + } + + public getUserTeamIds(email: string): string[] { + return ( + this.getTeamMembers().find(teamMember => teamMember.uniqueName === email) + ?.memberOf ?? [] + ); + } + + public async getAllTeams(): Promise { + if (!this.teams.size) { + await this.readTeams(); + } + + return Array.from(this.teams.values()); + } + + public getTeamMembers(): TeamMember[] { + return Array.from(this.teamMembers.values()); + } +} diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 99c140a424..6482ed2f59 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -24,6 +24,7 @@ import { WebApi, getPersonalAccessTokenHandler } from 'azure-devops-node-api'; import { AzureDevOpsApi } from '../api'; import { Config } from '@backstage/config'; import { Logger } from 'winston'; +import { PullRequestsDashboardProvider } from '../api/PullRequestsDashboardProvider'; import Router from 'express-promise-router'; import { errorHandler } from '@backstage/backend-common'; import express from 'express'; @@ -52,6 +53,9 @@ export async function createRouter( const azureDevOpsApi = options.azureDevOpsApi || new AzureDevOpsApi(logger, webApi); + const pullRequestsDashboardProvider = + await PullRequestsDashboardProvider.create(logger, azureDevOpsApi); + const router = Router(); router.use(express.json()); @@ -131,7 +135,7 @@ export async function createRouter( }; const pullRequests: DashboardPullRequest[] = - await azureDevOpsApi.getDashboardPullRequests( + await pullRequestsDashboardProvider.getDashboardPullRequests( projectName, pullRequestOptions, ); @@ -140,7 +144,7 @@ export async function createRouter( }); router.get('/all-teams', async (_req, res) => { - const allTeams = await azureDevOpsApi.getAllTeams(); + const allTeams = await pullRequestsDashboardProvider.getAllTeams(); res.status(200).json(allTeams); }); @@ -170,6 +174,12 @@ export async function createRouter( res.status(200).json(builds); }); + router.get('/users/:userId/team-ids', async (req, res) => { + const { userId } = req.params; + const teamIds = pullRequestsDashboardProvider.getUserTeamIds(userId); + res.status(200).json(teamIds); + }); + router.use(errorHandler()); return router; } diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index 85a9e54847..308fd54c61 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -270,9 +270,27 @@ export interface Team { // (undocumented) id?: string; // (undocumented) - memberIds?: string[]; + members?: string[]; // (undocumented) name?: string; + // (undocumented) + projectId?: string; + // (undocumented) + projectName?: string; +} + +// Warning: (ae-missing-release-tag) "TeamMember" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface TeamMember { + // (undocumented) + displayName?: string; + // (undocumented) + id?: string; + // (undocumented) + memberOf?: string[]; + // (undocumented) + uniqueName?: string; } // (No @packageDocumentation comment for this package) diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index 0befcf0b60..7978e3326a 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -178,7 +178,16 @@ export interface Repository { export interface Team { id?: string; name?: string; - memberIds?: string[]; + projectId?: string; + projectName?: string; + members?: string[]; +} + +export interface TeamMember { + id?: string; + displayName?: string; + uniqueName?: string; + memberOf?: string[]; } /** diff --git a/plugins/azure-devops/src/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts index 80d36f0adf..d87986c7f7 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsApi.ts @@ -49,4 +49,6 @@ export interface AzureDevOpsApi { ): Promise; getAllTeams(): Promise; + + getUserTeamIds(userId: string): Promise; } diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index 28d7e19cff..42c5ac3f6c 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -88,6 +88,10 @@ export class AzureDevOpsClient implements AzureDevOpsApi { return this.get('all-teams'); } + public getUserTeamIds(userId: string): Promise { + return this.get(`users/${userId}/team-ids`); + } + private async get(path: string): Promise { const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`; const url = new URL(path, baseUrl);