diff --git a/.changeset/brown-beers-share.md b/.changeset/brown-beers-share.md new file mode 100644 index 0000000000..9ff4f0aba4 --- /dev/null +++ b/.changeset/brown-beers-share.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-azure-common': patch +--- + +`getAllTeams` now accepts an optional `limit` parameter which can be used to return more than the default limit of 100 teams from the Azure DevOps API + +`pullRequestOptions` have been equipped with `teamsLimit` so that the property can be used with `getAllTeams` diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 7231509fc1..429a801b04 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -56,7 +56,7 @@ export class AzureDevOpsApi { }, ): AzureDevOpsApi; // (undocumented) - getAllTeams(): Promise; + getAllTeams(options?: { limit?: number }): Promise; // (undocumented) getBuildDefinitions( projectName: string, diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 18bad8c330..be0ec333bb 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -397,12 +397,18 @@ export class AzureDevOpsApi { .filter((policy): policy is Policy => Boolean(policy)); } - public async getAllTeams(): Promise { + public async getAllTeams(options?: { limit?: number }): Promise { this.logger?.debug('Getting all teams.'); const webApi = await this.getWebApi(); const client = await webApi.getCoreApi(); - const webApiTeams: WebApiTeam[] = await client.getAllTeams(); + + const webApiTeams: WebApiTeam[] = await client.getAllTeams( + undefined, + options?.limit, + undefined, + undefined, + ); const teams: Team[] = webApiTeams.map(team => ({ id: team.id, diff --git a/plugins/azure-devops-backend/src/api/PullRequestsDashboardProvider.ts b/plugins/azure-devops-backend/src/api/PullRequestsDashboardProvider.ts index 43b4807e7d..ec36d17e8c 100644 --- a/plugins/azure-devops-backend/src/api/PullRequestsDashboardProvider.ts +++ b/plugins/azure-devops-backend/src/api/PullRequestsDashboardProvider.ts @@ -25,6 +25,8 @@ import { AzureDevOpsApi } from './AzureDevOpsApi'; import { Logger } from 'winston'; import limiterFactory from 'p-limit'; +export const DEFAULT_TEAMS_LIMIT = 100; + export class PullRequestsDashboardProvider { private teams = new Map(); @@ -43,10 +45,10 @@ export class PullRequestsDashboardProvider { return provider; } - public async readTeams(): Promise { + public async readTeams(limit?: number): Promise { this.logger.info('Reading teams.'); - let teams = await this.azureDevOpsApi.getAllTeams(); + let teams = await this.azureDevOpsApi.getAllTeams({ limit }); // This is used to filter out the default Azure Devops project teams. teams = teams.filter(team => @@ -110,7 +112,7 @@ export class PullRequestsDashboardProvider { const dashboardPullRequests = await this.azureDevOpsApi.getDashboardPullRequests(projectName, options); - await this.getAllTeams(); // Make sure team members are loaded + await this.getAllTeams({ limit: options.teamsLimit }); // Make sure team members are loaded return dashboardPullRequests.map(pr => { if (pr.createdBy?.id) { @@ -126,7 +128,7 @@ export class PullRequestsDashboardProvider { } public async getUserTeamIds(email: string): Promise { - await this.getAllTeams(); // Make sure team members are loaded + await this.getAllTeams({}); // Make sure team members are loaded return ( Array.from(this.teamMembers.values()).find( teamMember => teamMember.uniqueName === email, @@ -134,9 +136,10 @@ export class PullRequestsDashboardProvider { ); } - public async getAllTeams(): Promise { + public async getAllTeams(options: { limit?: number }): Promise { if (!this.teams.size) { - await this.readTeams(); + const maxTeams = options?.limit ?? DEFAULT_TEAMS_LIMIT; + await this.readTeams(maxTeams); } return Array.from(this.teams.values()); diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index d1e9179dc1..a906ec5867 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -348,7 +348,74 @@ describe('createRouter', () => { expect(azureDevOpsApi.getPullRequests).toHaveBeenCalledWith( 'myProject', 'myRepo', - { status: 1, top: 50 }, + { status: 1, top: 50, teamsLimit: 100 }, + undefined, + undefined, + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(pullRequests); + }); + it('fetches a list of pull requests when using teamsLimit', async () => { + const firstPullRequest: PullRequest = { + pullRequestId: 7181, + repoName: 'super-feature-repo', + title: 'My Awesome New Feature', + createdBy: 'Jane Doe', + creationDate: '2020-09-12T06:10:23.932Z', + sourceRefName: 'refs/heads/topic/super-awesome-feature', + targetRefName: 'refs/heads/main', + status: PullRequestStatus.Active, + isDraft: false, + link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7181', + }; + + const secondPullRequest: PullRequest = { + pullRequestId: 7182, + repoName: 'super-feature-repo', + title: 'Refactoring My Awesome New Feature', + createdBy: 'Jane Doe', + creationDate: '2020-09-12T06:10:23.932Z', + sourceRefName: 'refs/heads/topic/refactor-super-awesome-feature', + targetRefName: 'refs/heads/main', + status: PullRequestStatus.Active, + isDraft: false, + link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7182', + }; + + const thirdPullRequest: PullRequest = { + pullRequestId: 7183, + repoName: 'super-feature-repo', + title: 'Bug Fix for My Awesome New Feature', + createdBy: 'Jane Doe', + creationDate: '2020-09-12T06:10:23.932Z', + sourceRefName: 'refs/heads/topic/fix-super-awesome-feature', + targetRefName: 'refs/heads/main', + status: PullRequestStatus.Active, + isDraft: false, + link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7183', + }; + + const pullRequests: PullRequest[] = [ + firstPullRequest, + secondPullRequest, + thirdPullRequest, + ]; + + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); + + azureDevOpsApi.getPullRequests.mockResolvedValueOnce(pullRequests); + + const response = await request(app) + .get('/pull-requests/myProject/myRepo') + .query({ entityRef: 'component:default/mycomponent' }) + .query({ top: '50', status: 1, teamsLimit: 50 }); + + expect(azureDevOpsApi.getPullRequests).toHaveBeenCalledWith( + 'myProject', + 'myRepo', + { status: 1, top: 50, teamsLimit: 50 }, undefined, undefined, ); diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 0697481e36..8a3daf9f9a 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -23,7 +23,10 @@ import { import { AzureDevOpsApi } from '../api'; import { Config } from '@backstage/config'; import { Logger } from 'winston'; -import { PullRequestsDashboardProvider } from '../api/PullRequestsDashboardProvider'; +import { + PullRequestsDashboardProvider, + DEFAULT_TEAMS_LIMIT, +} from '../api/PullRequestsDashboardProvider'; import Router from 'express-promise-router'; import { errorHandler, UrlReader } from '@backstage/backend-common'; import express from 'express'; @@ -175,6 +178,9 @@ export async function createRouter( const { projectName, repoName } = req.params; const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; + const teamsLimit = req.query.teamsLimit + ? Number(req.query.teamsLimit) + : DEFAULT_TEAMS_LIMIT; const host = req.query.host?.toString(); const org = req.query.org?.toString(); const status = req.query.status @@ -184,6 +190,7 @@ export async function createRouter( const pullRequestOptions: PullRequestOptions = { top: top, status: status, + teamsLimit: teamsLimit, }; const entityRef = req.query.entityRef; @@ -227,6 +234,9 @@ export async function createRouter( const { projectName } = req.params; const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; + const teamsLimit = req.query.teamsLimit + ? Number(req.query.teamsLimit) + : DEFAULT_TEAMS_LIMIT; const status = req.query.status ? Number(req.query.status) @@ -235,6 +245,7 @@ export async function createRouter( const pullRequestOptions: PullRequestOptions = { top: top, status: status, + teamsLimit: teamsLimit, }; const token = getBearerTokenFromAuthorizationHeader( @@ -266,8 +277,9 @@ export async function createRouter( res.status(200).json(pullRequests); }); - router.get('/all-teams', async (_req, res) => { - const allTeams = await pullRequestsDashboardProvider.getAllTeams(); + router.get('/all-teams', async (req, res) => { + const limit = req.query.limit ? Number(req.query.limit) : undefined; + const allTeams = await pullRequestsDashboardProvider.getAllTeams({ limit }); res.status(200).json(allTeams); }); diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index c926823367..af79d419da 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -215,6 +215,7 @@ export type PullRequest = { export type PullRequestOptions = { top: number; status: PullRequestStatus; + teamsLimit?: number; }; // @public (undocumented) diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index ab53af66cb..fc98255629 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -142,6 +142,7 @@ export type PullRequest = { export type PullRequestOptions = { top: number; status: PullRequestStatus; + teamsLimit?: number; }; /** @public */ diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md index e76e172d9b..4c91fba9e3 100644 --- a/plugins/azure-devops/api-report.md +++ b/plugins/azure-devops/api-report.md @@ -65,7 +65,7 @@ export type AssignedToUserFilter = BaseFilter & // @public (undocumented) export interface AzureDevOpsApi { // (undocumented) - getAllTeams(): Promise; + getAllTeams(limit?: number): Promise; // (undocumented) getBuildRuns( projectName: string, @@ -81,6 +81,7 @@ export interface AzureDevOpsApi { // (undocumented) getDashboardPullRequests( projectName: string, + teamsLimit?: number, ): Promise; // (undocumented) getGitTags( @@ -126,7 +127,7 @@ export const azureDevOpsApiRef: ApiRef; export class AzureDevOpsClient implements AzureDevOpsApi { constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }); // (undocumented) - getAllTeams(): Promise; + getAllTeams(limit?: number): Promise; // (undocumented) getBuildRuns( projectName: string, @@ -142,6 +143,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { // (undocumented) getDashboardPullRequests( projectName: string, + teamsLimit?: number, ): Promise; // (undocumented) getGitTags( @@ -193,6 +195,7 @@ export const AzurePullRequestsPage: (props: { projectName?: string | undefined; pollingInterval?: number | undefined; defaultColumnConfigs?: PullRequestColumnConfig[] | undefined; + teamsLimit?: number | undefined; }) => JSX_2.Element; // @public (undocumented) diff --git a/plugins/azure-devops/src/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts index 6dd6eeae6b..9f9ff1209f 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsApi.ts @@ -64,9 +64,10 @@ export interface AzureDevOpsApi { getDashboardPullRequests( projectName: string, + teamsLimit?: number, ): Promise; - getAllTeams(): Promise; + getAllTeams(limit?: number): Promise; getUserTeamIds(userId: string): Promise; diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index 9163836e0f..fb9d96c7e9 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -107,6 +107,9 @@ export class AzureDevOpsClient implements AzureDevOpsApi { if (options?.status) { queryString.append('status', options.status.toString()); } + if (options?.teamsLimit) { + queryString.append('teamsLimit', options.teamsLimit.toString()); + } if (host) { queryString.append('host', host); } @@ -124,14 +127,27 @@ export class AzureDevOpsClient implements AzureDevOpsApi { public getDashboardPullRequests( projectName: string, + teamsLimit?: number, ): Promise { - return this.get( - `dashboard-pull-requests/${projectName}?top=100`, - ); + const queryString = new URLSearchParams(); + queryString.append('top', '100'); + if (teamsLimit) { + queryString.append('teamsLimit', teamsLimit.toString()); + } + const urlSegment = `dashboard-pull-requests/${projectName}?${queryString}`; + return this.get(urlSegment); } - public getAllTeams(): Promise { - return this.get('all-teams'); + public getAllTeams(limit?: number): Promise { + const queryString = new URLSearchParams(); + if (limit) { + queryString.append('limit', limit.toString()); + } + let urlSegment = 'all-teams'; + if (queryString.toString()) { + urlSegment += `?${queryString}`; + } + return this.get(urlSegment); } public getUserTeamIds(userId: string): Promise { diff --git a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx index 5971a03afd..ed35545812 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx +++ b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx @@ -70,14 +70,17 @@ type PullRequestsPageProps = { projectName?: string; pollingInterval?: number; defaultColumnConfigs?: PullRequestColumnConfig[]; + teamsLimit?: number; }; export const PullRequestsPage = (props: PullRequestsPageProps) => { - const { projectName, pollingInterval, defaultColumnConfigs } = props; + const { projectName, pollingInterval, defaultColumnConfigs, teamsLimit } = + props; const { pullRequests, loading, error } = useDashboardPullRequests( projectName, pollingInterval, + teamsLimit, ); const [columnConfigs] = useState( diff --git a/plugins/azure-devops/src/hooks/useDashboardPullRequests.ts b/plugins/azure-devops/src/hooks/useDashboardPullRequests.ts index 8ace9663fa..f812b2abbb 100644 --- a/plugins/azure-devops/src/hooks/useDashboardPullRequests.ts +++ b/plugins/azure-devops/src/hooks/useDashboardPullRequests.ts @@ -27,6 +27,7 @@ const POLLING_INTERVAL = 10000; export function useDashboardPullRequests( project?: string, pollingInterval: number = POLLING_INTERVAL, + teamsLimit?: number, ): { pullRequests?: DashboardPullRequest[]; loading: boolean; @@ -43,7 +44,7 @@ export function useDashboardPullRequests( } try { - return await api.getDashboardPullRequests(project); + return await api.getDashboardPullRequests(project, teamsLimit); } catch (error) { if (error instanceof Error) { errorApi.post(error); @@ -51,7 +52,7 @@ export function useDashboardPullRequests( return Promise.reject(error); } - }, [project, api, errorApi]); + }, [project, api, teamsLimit, errorApi]); const { value: pullRequests, diff --git a/plugins/azure-devops/src/hooks/usePullRequests.ts b/plugins/azure-devops/src/hooks/usePullRequests.ts index 636e7d9562..f4145b59d7 100644 --- a/plugins/azure-devops/src/hooks/usePullRequests.ts +++ b/plugins/azure-devops/src/hooks/usePullRequests.ts @@ -31,16 +31,19 @@ export function usePullRequests( entity: Entity, defaultLimit?: number, requestedStatus?: PullRequestStatus, + defaultTeamsLimit?: number, ): { items?: PullRequest[]; loading: boolean; error?: Error; } { const top = defaultLimit ?? AZURE_DEVOPS_DEFAULT_TOP; + const teamsLimit = defaultTeamsLimit ?? undefined; const status = requestedStatus ?? PullRequestStatus.Active; const options: PullRequestOptions = { top, status, + teamsLimit, }; const api = useApi(azureDevOpsApiRef);