Merge pull request #8517 from marleypowell/marley/7678-pull-request-custom-filters-2
feat: Added the ability to resolve team and team member relations for PR column filters
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/plugin-azure-devops-backend': minor
|
||||
'@backstage/plugin-azure-devops': patch
|
||||
'@backstage/plugin-azure-devops-common': minor
|
||||
---
|
||||
|
||||
- feat: Created PullRequestsDashboardProvider for resolving team and team member relations
|
||||
- feat: Created useUserTeamIds hook.
|
||||
- feat: Updated useFilterProcessor to provide teamIds for `AssignedToCurrentUsersTeams` and `CreatedByCurrentUsersTeams` filters.
|
||||
@@ -15,6 +15,7 @@ import { PullRequest } from '@backstage/plugin-azure-devops-common';
|
||||
import { PullRequestOptions } from '@backstage/plugin-azure-devops-common';
|
||||
import { RepoBuild } from '@backstage/plugin-azure-devops-common';
|
||||
import { Team } from '@backstage/plugin-azure-devops-common';
|
||||
import { TeamMember } from '@backstage/plugin-azure-devops-common';
|
||||
import { WebApi } from 'azure-devops-node-api';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AzureDevOpsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -71,6 +72,14 @@ export class AzureDevOpsApi {
|
||||
repoName: string,
|
||||
top: number,
|
||||
): Promise<RepoBuild[]>;
|
||||
// (undocumented)
|
||||
getTeamMembers({
|
||||
projectId,
|
||||
teamId,
|
||||
}: {
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
}): Promise<TeamMember[] | undefined>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"azure-devops-node-api": "^11.0.1",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"p-limit": "^3.1.0",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -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,39 +229,37 @@ 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<string[] | undefined> {
|
||||
this.logger?.debug(`Getting team member ids for team '${team.name}'.`);
|
||||
|
||||
if (!team.projectId || !team.id) {
|
||||
return undefined;
|
||||
}
|
||||
public async getTeamMembers({
|
||||
projectId,
|
||||
teamId,
|
||||
}: {
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
}): Promise<TeamMember[] | undefined> {
|
||||
this.logger?.debug(`Getting team member ids for team '${teamId}'.`);
|
||||
|
||||
const client = await this.webApi.getCoreApi();
|
||||
|
||||
const teamMembers: TeamMember[] =
|
||||
await client.getTeamMembersWithExtendedProperties(
|
||||
team.projectId,
|
||||
team.id,
|
||||
);
|
||||
const teamMembers: AdoTeamMember[] =
|
||||
await client.getTeamMembersWithExtendedProperties(projectId, teamId);
|
||||
|
||||
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(
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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';
|
||||
import limiterFactory from 'p-limit';
|
||||
|
||||
export class PullRequestsDashboardProvider {
|
||||
private teams = new Map<string, Team>();
|
||||
|
||||
private teamMembers = new Map<string, TeamMember>();
|
||||
|
||||
private constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly azureDevOpsApi: AzureDevOpsApi,
|
||||
) {}
|
||||
|
||||
public static async create(
|
||||
logger: Logger,
|
||||
azureDevOpsApi: AzureDevOpsApi,
|
||||
): Promise<PullRequestsDashboardProvider> {
|
||||
const provider = new PullRequestsDashboardProvider(logger, azureDevOpsApi);
|
||||
await provider.readTeams();
|
||||
return provider;
|
||||
}
|
||||
|
||||
public async readTeams(): Promise<void> {
|
||||
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<string, Team>();
|
||||
this.teamMembers = new Map<string, TeamMember>();
|
||||
|
||||
const limiter = limiterFactory(5);
|
||||
|
||||
await Promise.all(
|
||||
teams.map(team =>
|
||||
limiter(async () => {
|
||||
const teamId = team.id;
|
||||
const projectId = team.projectId;
|
||||
|
||||
if (teamId) {
|
||||
let teamMembers: TeamMember[] | undefined;
|
||||
|
||||
if (projectId) {
|
||||
teamMembers = await this.azureDevOpsApi.getTeamMembers({
|
||||
projectId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
||||
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<DashboardPullRequest[]> {
|
||||
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<Team[]> {
|
||||
if (!this.teams.size) {
|
||||
await this.readTeams();
|
||||
}
|
||||
|
||||
return Array.from(this.teams.values());
|
||||
}
|
||||
|
||||
public getTeamMembers(): TeamMember[] {
|
||||
return Array.from(this.teamMembers.values());
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,8 @@ describe('createRouter', () => {
|
||||
getPullRequests: jest.fn(),
|
||||
getBuilds: jest.fn(),
|
||||
getBuildRuns: jest.fn(),
|
||||
getAllTeams: jest.fn().mockReturnValue([]),
|
||||
getTeamMembers: jest.fn(),
|
||||
} as any;
|
||||
const router = await createRouter({
|
||||
azureDevOpsApi,
|
||||
@@ -406,4 +408,11 @@ describe('createRouter', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /users/:userId/team-ids', () => {
|
||||
it('fetches a a list of teams', async () => {
|
||||
const response = await request(app).get('/users/user1/team-ids');
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,4 +47,6 @@ export interface AzureDevOpsApi {
|
||||
): Promise<DashboardPullRequest[]>;
|
||||
|
||||
getAllTeams(): Promise<Team[]>;
|
||||
|
||||
getUserTeamIds(userId: string): Promise<string[]>;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,10 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
|
||||
return this.get<Team[]>('all-teams');
|
||||
}
|
||||
|
||||
public getUserTeamIds(userId: string): Promise<string[]> {
|
||||
return this.get<string[]>(`users/${userId}/team-ids`);
|
||||
}
|
||||
|
||||
private async get<T>(path: string): Promise<T> {
|
||||
const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`;
|
||||
const url = new URL(path, baseUrl);
|
||||
|
||||
+7
-2
@@ -15,11 +15,11 @@
|
||||
*/
|
||||
|
||||
import { Filter, FilterType } from '../filters';
|
||||
|
||||
import { useUserEmail } from '../../../../hooks';
|
||||
import { useUserEmail, useUserTeamIds } from '../../../../hooks';
|
||||
|
||||
export function useFilterProcessor(): (filters: Filter[]) => Filter[] {
|
||||
const userEmail = useUserEmail();
|
||||
const { teamIds } = useUserTeamIds(userEmail);
|
||||
|
||||
return (filters: Filter[]): Filter[] => {
|
||||
for (const filter of filters) {
|
||||
@@ -29,6 +29,11 @@ export function useFilterProcessor(): (filters: Filter[]) => Filter[] {
|
||||
filter.email = userEmail;
|
||||
break;
|
||||
|
||||
case FilterType.AssignedToCurrentUsersTeams:
|
||||
case FilterType.CreatedByCurrentUsersTeams:
|
||||
filter.teamIds = teamIds;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -20,3 +20,4 @@ export * from './useProjectRepoFromEntity';
|
||||
export * from './usePullRequests';
|
||||
export * from './useRepoBuilds';
|
||||
export * from './useUserEmail';
|
||||
export * from './useUserTeamIds';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { azureDevOpsApiRef } from '../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
export function useUserTeamIds(userId: string | undefined): {
|
||||
teamIds?: string[];
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
} {
|
||||
const api = useApi(azureDevOpsApiRef);
|
||||
|
||||
const {
|
||||
value: teamIds,
|
||||
loading,
|
||||
error,
|
||||
} = useAsync(() => {
|
||||
return userId ? api.getUserTeamIds(userId) : Promise.resolve(undefined);
|
||||
}, [api]);
|
||||
|
||||
return {
|
||||
teamIds,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user