Merge pull request #8384 from marleypowell/marley/7678-pull-request-custom-filters

feat: Added ability to configure PR columns with parameters using filters.
This commit is contained in:
Ben Lambert
2021-12-15 10:58:26 +01:00
committed by GitHub
27 changed files with 1121 additions and 106 deletions
+58
View File
@@ -0,0 +1,58 @@
---
'@backstage/plugin-azure-devops': patch
'@backstage/plugin-azure-devops-backend': patch
'@backstage/plugin-azure-devops-common': patch
---
Created some initial filters that can be used to create pull request columns:
- All
- AssignedToUser
- AssignedToCurrentUser
- AssignedToTeam
- AssignedToTeams
- AssignedToCurrentUsersTeams
- CreatedByUser
- CreatedByCurrentUser
- CreatedByTeam
- CreatedByTeams
- CreatedByCurrentUsersTeams
Example custom column creation:
```tsx
const COLUMN_CONFIGS: PullRequestColumnConfig[] = [
{
title: 'Created by me',
filters: [{ type: FilterType.CreatedByCurrentUser }],
},
{
title: 'Created by Backstage Core',
filters: [
{
type: FilterType.CreatedByTeam,
teamName: 'Backstage Core',
},
],
},
{
title: 'Assigned to my teams',
filters: [{ type: FilterType.AssignedToCurrentUsersTeams }],
},
{
title: 'Other PRs',
filters: [{ type: FilterType.All }],
simplified: true,
},
];
<Route
path="/azure-pull-requests"
element={
<AzurePullRequestsPage
projectName="{PROJECT_NAME}"
defaultColumnConfigs={COLUMN_CONFIGS}
/>
}
/>;
```
@@ -215,6 +215,7 @@ function convertReviewer(
return {
id: identityRef.id,
displayName: identityRef.displayName,
uniqueName: identityRef.uniqueName,
imageUrl: getAvatarUrl(identityRef),
isRequired: identityRef.isRequired,
isContainer: identityRef.isContainer,
@@ -61,6 +61,10 @@ export interface CreatedBy {
// (undocumented)
imageUrl?: string;
// (undocumented)
teamIds?: string[];
// (undocumented)
teamNames?: string[];
// (undocumented)
uniqueName?: string;
}
@@ -254,6 +258,8 @@ export interface Reviewer {
// (undocumented)
isRequired?: boolean;
// (undocumented)
uniqueName?: string;
// (undocumented)
voteStatus: PullRequestVoteStatus;
}
+3
View File
@@ -145,6 +145,7 @@ export interface DashboardPullRequest {
export interface Reviewer {
id?: string;
displayName?: string;
uniqueName?: string;
imageUrl?: string;
isRequired?: boolean;
isContainer?: boolean;
@@ -164,6 +165,8 @@ export interface CreatedBy {
displayName?: string;
uniqueName?: string;
imageUrl?: string;
teamIds?: string[];
teamNames?: string[];
}
export interface Repository {
+163
View File
@@ -6,9 +6,55 @@
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { Entity } from '@backstage/catalog-model';
import { SvgIconProps } from '@material-ui/core';
// Warning: (ae-missing-release-tag) "AllFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AllFilter = BaseFilter & {
type: FilterType.All;
};
// Warning: (ae-missing-release-tag) "AssignedToTeamFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AssignedToTeamFilter = BaseFilter & {
type: FilterType.AssignedToTeam;
teamId: string;
};
// Warning: (ae-missing-release-tag) "AssignedToTeamsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AssignedToTeamsFilter = BaseFilter &
(
| {
type: FilterType.AssignedToTeams;
teamIds: string[];
}
| {
type: FilterType.AssignedToCurrentUsersTeams;
teamIds?: string[];
}
);
// Warning: (ae-missing-release-tag) "AssignedToUserFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AssignedToUserFilter = BaseFilter &
(
| {
type: FilterType.AssignedToUser;
email: string;
}
| {
type: FilterType.AssignedToCurrentUser;
email?: string;
}
);
// Warning: (ae-missing-release-tag) "azureDevOpsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -25,11 +71,71 @@ export const AzurePullRequestsIcon: (props: SvgIconProps) => JSX.Element;
export const AzurePullRequestsPage: ({
projectName,
pollingInterval,
defaultColumnConfigs,
}: {
projectName?: string | undefined;
pollingInterval?: number | undefined;
defaultColumnConfigs?: PullRequestColumnConfig[] | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "BaseFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type BaseFilter = {
type: FilterType;
};
// Warning: (ae-missing-release-tag) "CreatedByTeamFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type CreatedByTeamFilter = BaseFilter &
({
type: FilterType.CreatedByTeam;
} & (
| {
teamId: string;
}
| {
teamName: string;
}
));
// Warning: (ae-missing-release-tag) "CreatedByTeamsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type CreatedByTeamsFilter = BaseFilter &
(
| ({
type: FilterType.CreatedByTeams;
} & (
| {
teamIds: string[];
}
| {
teamNames: string[];
}
))
| {
type: FilterType.CreatedByCurrentUsersTeams;
teamIds?: string[];
}
);
// Warning: (ae-missing-release-tag) "CreatedByUserFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type CreatedByUserFilter = BaseFilter &
(
| {
type: FilterType.CreatedByUser;
email: string;
}
| {
type: FilterType.CreatedByCurrentUser;
email?: string;
}
);
// Warning: (ae-missing-release-tag) "EntityAzurePipelinesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -48,10 +154,67 @@ export const EntityAzurePullRequestsContent: ({
defaultLimit?: number | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type Filter =
| AssignedToUserFilter
| CreatedByUserFilter
| AssignedToTeamFilter
| CreatedByTeamFilter
| AssignedToTeamsFilter
| CreatedByTeamsFilter
| AllFilter;
// Warning: (ae-missing-release-tag) "FilterType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum FilterType {
// (undocumented)
All = 'All',
// (undocumented)
AssignedToCurrentUser = 'AssignedToCurrentUser',
// (undocumented)
AssignedToCurrentUsersTeams = 'AssignedToCurrentUsersTeams',
// (undocumented)
AssignedToTeam = 'AssignedToTeam',
// (undocumented)
AssignedToTeams = 'AssignedToTeams',
// (undocumented)
AssignedToUser = 'AssignedToUser',
// (undocumented)
CreatedByCurrentUser = 'CreatedByCurrentUser',
// (undocumented)
CreatedByCurrentUsersTeams = 'CreatedByCurrentUsersTeams',
// (undocumented)
CreatedByTeam = 'CreatedByTeam',
// (undocumented)
CreatedByTeams = 'CreatedByTeams',
// (undocumented)
CreatedByUser = 'CreatedByUser',
}
// Warning: (ae-missing-release-tag) "isAzureDevOpsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const isAzureDevOpsAvailable: (entity: Entity) => boolean;
// Warning: (ae-missing-release-tag) "PullRequestColumnConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface PullRequestColumnConfig {
// (undocumented)
filters: Filter[];
// (undocumented)
simplified?: boolean;
// (undocumented)
title: string;
}
// Warning: (ae-missing-release-tag) "PullRequestFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PullRequestFilter = (pullRequest: DashboardPullRequest) => boolean;
// (No @packageDocumentation comment for this package)
```
@@ -21,56 +21,17 @@ import {
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import { PullRequestGroup, PullRequestGroupConfig } from './lib/types';
import React, { useEffect, useState } from 'react';
import { getCreatedByUserFilter, getPullRequestGroups } from './lib/utils';
import { useDashboardPullRequests, useUserEmail } from '../../hooks';
import { PullRequestColumnConfig, PullRequestGroup } from './lib/types';
import React, { useState } from 'react';
import { getPullRequestGroupConfigs, getPullRequestGroups } from './lib/utils';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { FilterType } from './lib/filters';
import { PullRequestGrid } from './lib/PullRequestGrid';
function usePullRequestGroupConfigs(
userEmail: string | undefined,
): PullRequestGroupConfig[] {
const [pullRequestGroupConfigs, setPullRequestGroupConfigs] = useState<
PullRequestGroupConfig[]
>([]);
useEffect(() => {
const prGroupConfigs: PullRequestGroupConfig[] = [
{ title: 'Created by me', filter: getCreatedByUserFilter(userEmail) },
{ title: 'Other PRs', filter: _ => true, simplified: false },
];
setPullRequestGroupConfigs(prGroupConfigs);
}, [userEmail]);
return pullRequestGroupConfigs;
}
function usePullRequestGroups(
pullRequests: DashboardPullRequest[] | undefined,
pullRequestGroupConfigs: PullRequestGroupConfig[],
): PullRequestGroup[] {
const [pullRequestGroups, setPullRequestGroups] = useState<
PullRequestGroup[]
>([]);
useEffect(() => {
if (pullRequests) {
const groups = getPullRequestGroups(
pullRequests,
pullRequestGroupConfigs,
);
setPullRequestGroups(groups);
}
}, [pullRequests, pullRequestGroupConfigs]);
return pullRequestGroups;
}
import { useDashboardPullRequests } from '../../hooks';
import { useFilterProcessor } from './lib/hooks';
type PullRequestsPageContentProps = {
pullRequestGroups: PullRequestGroup[];
pullRequestGroups: PullRequestGroup[] | undefined;
loading: boolean;
error?: Error;
};
@@ -80,7 +41,7 @@ const PullRequestsPageContent = ({
loading,
error,
}: PullRequestsPageContentProps) => {
if (loading && pullRequestGroups.length <= 0) {
if (loading && (!pullRequestGroups || pullRequestGroups.length <= 0)) {
return <Progress />;
}
@@ -88,25 +49,50 @@ const PullRequestsPageContent = ({
return <ResponseErrorPanel error={error} />;
}
return <PullRequestGrid pullRequestGroups={pullRequestGroups} />;
return <PullRequestGrid pullRequestGroups={pullRequestGroups ?? []} />;
};
const DEFAULT_COLUMN_CONFIGS: PullRequestColumnConfig[] = [
{
title: 'Created by me',
filters: [{ type: FilterType.CreatedByCurrentUser }],
simplified: false,
},
{
title: 'Other PRs',
filters: [{ type: FilterType.All }],
simplified: true,
},
];
type PullRequestsPageProps = {
projectName?: string;
pollingInterval?: number;
defaultColumnConfigs?: PullRequestColumnConfig[];
};
export const PullRequestsPage = ({
projectName,
pollingInterval,
defaultColumnConfigs,
}: PullRequestsPageProps) => {
const { pullRequests, loading, error } = useDashboardPullRequests(
projectName,
pollingInterval,
);
const userEmail = useUserEmail();
const pullRequestGroupConfigs = usePullRequestGroupConfigs(userEmail);
const pullRequestGroups = usePullRequestGroups(
const [columnConfigs] = useState(
defaultColumnConfigs ?? DEFAULT_COLUMN_CONFIGS,
);
const filterProcessor = useFilterProcessor();
const pullRequestGroupConfigs = getPullRequestGroupConfigs(
columnConfigs,
filterProcessor,
);
const pullRequestGroups = getPullRequestGroups(
pullRequests,
pullRequestGroupConfigs,
);
@@ -15,3 +15,17 @@
*/
export { PullRequestsPage } from './PullRequestsPage';
export type { PullRequestColumnConfig } from './lib/types';
export { FilterType } from './lib/filters';
export type {
BaseFilter,
Filter,
PullRequestFilter,
AssignedToUserFilter,
CreatedByUserFilter,
AssignedToTeamFilter,
CreatedByTeamFilter,
AssignedToTeamsFilter,
CreatedByTeamsFilter,
AllFilter,
} from './lib/filters';
@@ -0,0 +1,27 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
export type AllFilter = BaseFilter & {
type: FilterType.All;
};
export function createAllFilter(): PullRequestFilter {
return (_pullRequest: DashboardPullRequest): boolean => true;
}
@@ -0,0 +1,39 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { stringArrayHas } from '../../../../utils';
export type AssignedToTeamFilter = BaseFilter & {
type: FilterType.AssignedToTeam;
teamId: string;
};
export function createAssignedToTeamFilter(
filter: AssignedToTeamFilter,
): PullRequestFilter {
return (pullRequest: DashboardPullRequest): boolean => {
const reviewerIds = pullRequest.reviewers?.map(reviewer => reviewer.id);
if (!reviewerIds) {
return false;
}
return stringArrayHas(reviewerIds, filter.teamId, true);
};
}
@@ -0,0 +1,51 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { createAssignedToTeamFilter } from './assignedToTeamFilter';
export type AssignedToTeamsFilter = BaseFilter &
(
| {
type: FilterType.AssignedToTeams;
teamIds: string[];
}
| {
type: FilterType.AssignedToCurrentUsersTeams;
teamIds?: string[];
}
);
export function createAssignedToTeamsFilter(
filter: AssignedToTeamsFilter,
): PullRequestFilter {
const teamIds = filter.teamIds;
return (pullRequest: DashboardPullRequest): boolean => {
if (!teamIds) {
return false;
}
return teamIds.some(teamId => {
return createAssignedToTeamFilter({
type: FilterType.AssignedToTeam,
teamId,
})(pullRequest);
});
};
}
@@ -0,0 +1,50 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { stringArrayHas } from '../../../../utils';
export type AssignedToUserFilter = BaseFilter &
(
| {
type: FilterType.AssignedToUser;
email: string;
}
| {
type: FilterType.AssignedToCurrentUser;
email?: string;
}
);
export function createAssignedToUserFilter(
filter: AssignedToUserFilter,
): PullRequestFilter {
const email = filter.email;
return (pullRequest: DashboardPullRequest): boolean => {
const uniqueNames = pullRequest.reviewers?.map(
reviewer => reviewer.uniqueName,
);
if (!email || !uniqueNames) {
return false;
}
return stringArrayHas(uniqueNames, email, true);
};
}
@@ -0,0 +1,150 @@
/*
* 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 { Filter, FilterType } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { createFilter } from './createFilter';
describe('createFilter', () => {
const pullRequest = {
createdBy: {
uniqueName: 'user1@backstage.com',
teamIds: ['team1Id', 'team2Id'],
},
reviewers: [
{ uniqueName: 'user2@backstage.com' },
{ id: 'team2Id' },
{ id: 'team3Id' },
],
} as DashboardPullRequest;
const testCases: Array<{ filter: Filter; result: boolean }> = [
{
filter: {
type: FilterType.AssignedToUser,
email: 'user2@backstage.com',
},
result: true,
},
{
filter: {
type: FilterType.AssignedToUser,
email: 'random-user@backstage.com',
},
result: false,
},
{
filter: {
type: FilterType.CreatedByUser,
email: 'user1@backstage.com',
},
result: true,
},
{
filter: {
type: FilterType.CreatedByUser,
email: 'random-user@backstage.com',
},
result: false,
},
{
filter: {
type: FilterType.AssignedToTeam,
teamId: 'team2Id',
},
result: true,
},
{
filter: {
type: FilterType.AssignedToTeam,
teamId: 'randomTeamId',
},
result: false,
},
{
filter: {
type: FilterType.CreatedByTeam,
teamId: 'team1Id',
},
result: true,
},
{
filter: {
type: FilterType.CreatedByTeam,
teamId: 'randomTeamId',
},
result: false,
},
{
filter: {
type: FilterType.AssignedToTeams,
teamIds: ['team2Id', 'randomTeamId'],
},
result: true,
},
{
filter: {
type: FilterType.AssignedToTeams,
teamIds: ['team2Id', 'team3Id'],
},
result: true,
},
{
filter: {
type: FilterType.AssignedToTeams,
teamIds: ['randomTeam1Id', 'randomTeam2Id'],
},
result: false,
},
{
filter: {
type: FilterType.CreatedByTeams,
teamIds: ['team1Id', 'randomTeamId'],
},
result: true,
},
{
filter: {
type: FilterType.CreatedByTeams,
teamIds: ['team1Id', 'team2Id'],
},
result: true,
},
{
filter: {
type: FilterType.CreatedByTeams,
teamIds: ['randomTeam1Id', 'randomTeam2Id'],
},
result: false,
},
{
filter: {
type: FilterType.All,
},
result: true,
},
];
testCases.forEach(({ filter, result }) => {
it(`should return ${String(result)} when pull request ${
result ? 'is' : 'is not'
} ${filter.type}`, () => {
const pullRequestFilter = createFilter(filter);
expect(pullRequestFilter(pullRequest)).toBe(result);
});
});
});
@@ -0,0 +1,71 @@
/*
* 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 { Filter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { createAllFilter } from './allFilter';
import { createAssignedToTeamFilter } from './assignedToTeamFilter';
import { createAssignedToTeamsFilter } from './assignedToTeamsFilter';
import { createAssignedToUserFilter } from './assignedToUserFilter';
import { createCreatedByTeamFilter } from './createdByTeamFilter';
import { createCreatedByTeamsFilter } from './createdByTeamsFilter';
import { createCreatedByUserFilter } from './createdByUserFilter';
export function createFilter(filters: Filter | Filter[]): PullRequestFilter {
const mapFilter = (filter: Filter): PullRequestFilter => {
switch (filter.type) {
case FilterType.AssignedToUser:
case FilterType.AssignedToCurrentUser:
return createAssignedToUserFilter(filter);
case FilterType.CreatedByUser:
case FilterType.CreatedByCurrentUser:
return createCreatedByUserFilter(filter);
case FilterType.AssignedToTeam:
return createAssignedToTeamFilter(filter);
case FilterType.CreatedByTeam:
return createCreatedByTeamFilter(filter);
case FilterType.AssignedToTeams:
case FilterType.AssignedToCurrentUsersTeams:
return createAssignedToTeamsFilter(filter);
case FilterType.CreatedByTeams:
case FilterType.CreatedByCurrentUsersTeams:
return createCreatedByTeamsFilter(filter);
case FilterType.All:
return createAllFilter();
default:
return _ => false;
}
};
if (Array.isArray(filters)) {
if (filters.length === 1) {
return mapFilter(filters[0]);
}
return (pullRequest: DashboardPullRequest): boolean =>
filters.every(filter => mapFilter(filter)(pullRequest));
}
return mapFilter(filters);
}
@@ -0,0 +1,42 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { stringArrayHas } from '../../../../utils';
export type CreatedByTeamFilter = BaseFilter &
({
type: FilterType.CreatedByTeam;
} & ({ teamId: string } | { teamName: string }));
export function createCreatedByTeamFilter(
filter: CreatedByTeamFilter,
): PullRequestFilter {
return (pullRequest: DashboardPullRequest): boolean => {
const [createdByTeams, team] =
'teamId' in filter
? [pullRequest.createdBy?.teamIds, filter.teamId]
: [pullRequest.createdBy?.teamNames, filter.teamName];
if (!createdByTeams) {
return false;
}
return stringArrayHas(createdByTeams, team, true);
};
}
@@ -0,0 +1,65 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { createCreatedByTeamFilter } from './createdByTeamFilter';
export type CreatedByTeamsFilter = BaseFilter &
(
| ({
type: FilterType.CreatedByTeams;
} & ({ teamIds: string[] } | { teamNames: string[] }))
| {
type: FilterType.CreatedByCurrentUsersTeams;
teamIds?: string[];
}
);
export function createCreatedByTeamsFilter(
filter: CreatedByTeamsFilter,
): PullRequestFilter {
return (pullRequest: DashboardPullRequest): boolean => {
if ('teamNames' in filter) {
const teamNames = filter.teamNames;
if (!teamNames) {
return false;
}
return teamNames.some(teamName => {
return createCreatedByTeamFilter({
type: FilterType.CreatedByTeam,
teamName,
})(pullRequest);
});
}
const teamIds = filter.teamIds;
if (!teamIds) {
return false;
}
return teamIds.some(teamId => {
return createCreatedByTeamFilter({
type: FilterType.CreatedByTeam,
teamId,
})(pullRequest);
});
};
}
@@ -0,0 +1,48 @@
/*
* 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 { BaseFilter, FilterType, PullRequestFilter } from './types';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
import { equalsIgnoreCase } from '../../../../utils';
export type CreatedByUserFilter = BaseFilter &
(
| {
type: FilterType.CreatedByUser;
email: string;
}
| {
type: FilterType.CreatedByCurrentUser;
email?: string;
}
);
export function createCreatedByUserFilter(
filter: CreatedByUserFilter,
): PullRequestFilter {
const email = filter.email;
return (pullRequest: DashboardPullRequest): boolean => {
const uniqueName = pullRequest.createdBy?.uniqueName;
if (!email || !uniqueName) {
return false;
}
return equalsIgnoreCase(email, uniqueName);
};
}
@@ -0,0 +1,30 @@
/*
* 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.
*/
export { createFilter } from './createFilter';
export { FilterTypes, FilterType } from './types';
export type {
BaseFilter,
Filter,
PullRequestFilter,
AssignedToUserFilter,
CreatedByUserFilter,
AssignedToTeamFilter,
CreatedByTeamFilter,
AssignedToTeamsFilter,
CreatedByTeamsFilter,
AllFilter,
} from './types';
@@ -0,0 +1,83 @@
/*
* 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 { AllFilter } from './allFilter';
import { AssignedToTeamFilter } from './assignedToTeamFilter';
import { AssignedToTeamsFilter } from './assignedToTeamsFilter';
import { AssignedToUserFilter } from './assignedToUserFilter';
import { CreatedByTeamFilter } from './createdByTeamFilter';
import { CreatedByTeamsFilter } from './createdByTeamsFilter';
import { CreatedByUserFilter } from './createdByUserFilter';
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
export enum FilterType {
All = 'All',
// Assigned To
AssignedToUser = 'AssignedToUser',
AssignedToCurrentUser = 'AssignedToCurrentUser',
AssignedToTeam = 'AssignedToTeam',
AssignedToTeams = 'AssignedToTeams',
AssignedToCurrentUsersTeams = 'AssignedToCurrentUsersTeams',
// Created By
CreatedByUser = 'CreatedByUser',
CreatedByCurrentUser = 'CreatedByCurrentUser',
CreatedByTeam = 'CreatedByTeam',
CreatedByTeams = 'CreatedByTeams',
CreatedByCurrentUsersTeams = 'CreatedByCurrentUsersTeams',
}
export const FilterTypes = [
FilterType.All,
FilterType.AssignedToUser,
FilterType.AssignedToCurrentUser,
FilterType.AssignedToTeam,
FilterType.AssignedToTeams,
FilterType.AssignedToCurrentUsersTeams,
FilterType.CreatedByUser,
FilterType.CreatedByCurrentUser,
FilterType.CreatedByTeam,
FilterType.CreatedByTeams,
FilterType.CreatedByCurrentUsersTeams,
] as const;
export type BaseFilter = {
type: FilterType;
};
export type Filter =
| AssignedToUserFilter
| CreatedByUserFilter
| AssignedToTeamFilter
| CreatedByTeamFilter
| AssignedToTeamsFilter
| CreatedByTeamsFilter
| AllFilter;
export type {
AssignedToUserFilter,
CreatedByUserFilter,
AssignedToTeamFilter,
CreatedByTeamFilter,
AssignedToTeamsFilter,
CreatedByTeamsFilter,
AllFilter,
};
export type PullRequestFilter = (pullRequest: DashboardPullRequest) => boolean;
@@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './useFilterProcessor';
@@ -0,0 +1,39 @@
/*
* 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 { Filter, FilterType } from '../filters';
import { useUserEmail } from '../../../../hooks';
export function useFilterProcessor(): (filters: Filter[]) => Filter[] {
const userEmail = useUserEmail();
return (filters: Filter[]): Filter[] => {
for (const filter of filters) {
switch (filter.type) {
case FilterType.AssignedToCurrentUser:
case FilterType.CreatedByCurrentUser:
filter.email = userEmail;
break;
default:
break;
}
}
return filters;
};
}
@@ -14,23 +14,24 @@
* limitations under the License.
*/
import {
DashboardPullRequest,
Team,
} from '@backstage/plugin-azure-devops-common';
import { Filter, PullRequestFilter } from './filters';
export interface PullRequestGroup {
import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common';
export interface PullRequestColumnConfig {
title: string;
pullRequests: DashboardPullRequest[];
filters: Filter[];
simplified?: boolean;
}
export type PullRequestFilter = (pullRequest: DashboardPullRequest) => boolean;
export type TeamFilter = (team: Team) => boolean;
export interface PullRequestGroupConfig {
title: string;
filter: PullRequestFilter;
simplified?: boolean;
}
export interface PullRequestGroup {
title: string;
pullRequests: DashboardPullRequest[];
simplified?: boolean;
}
@@ -19,33 +19,7 @@ import {
PullRequestVoteStatus,
Reviewer,
} from '@backstage/plugin-azure-devops-common';
import {
arrayExtract,
getCreatedByUserFilter,
getPullRequestGroups,
reviewerFilter,
} from './utils';
describe('getCreatedByUserFilter', () => {
it('should filter if pull request is created by user', () => {
const userEmail = 'user1@backstage.com';
const pr = {
createdBy: { uniqueName: userEmail },
} as DashboardPullRequest;
const result = getCreatedByUserFilter(userEmail)(pr);
expect(result).toBe(true);
});
it('should not filter if pull request is not created by user', () => {
const userEmail1 = 'user1@backstage.com';
const userEmail2 = 'user2@backstage.com';
const pr = {
createdBy: { uniqueName: userEmail1 },
} as DashboardPullRequest;
const result = getCreatedByUserFilter(userEmail2)(pr);
expect(result).toBe(false);
});
});
import { arrayExtract, getPullRequestGroups, reviewerFilter } from './utils';
describe('reviewerFilter', () => {
it('should return false if reviewer has no vote and is not required', () => {
@@ -128,11 +102,15 @@ describe('getPullRequestGroups', () => {
];
const configs = [
{ title: 'Created by me', filter: getCreatedByUserFilter(userEmail) },
{
title: 'Created by me',
filter: (pullRequest: DashboardPullRequest): boolean =>
pullRequest.createdBy?.uniqueName === userEmail,
},
{ title: 'Other PRs', filter: (_: unknown) => true, simplified: true },
];
const result = getPullRequestGroups(pullRequests, configs);
const result = getPullRequestGroups(pullRequests, configs) ?? [];
expect(result.length).toBe(2);
@@ -19,25 +19,13 @@ import {
PullRequestVoteStatus,
Reviewer,
} from '@backstage/plugin-azure-devops-common';
import { Filter, createFilter } from './filters';
import {
PullRequestFilter,
PullRequestColumnConfig,
PullRequestGroup,
PullRequestGroupConfig,
} from './types';
/**
* Creates a filter that matches pull requests created by `userEmail`.
* @param userEmail an email to filter by.
* @returns a filter for pull requests created by `userEmail`.
*/
export function getCreatedByUserFilter(
userEmail: string | undefined,
): PullRequestFilter {
return (pullRequest: DashboardPullRequest): boolean =>
pullRequest.createdBy?.uniqueName?.toLocaleLowerCase() ===
userEmail?.toLocaleLowerCase();
}
/**
* Filters a reviewer based on vote status and if the reviewer is required.
* @param reviewer a reviewer to filter.
@@ -97,9 +85,13 @@ export function arrayExtract<T>(arr: T[], filter: (value: T) => unknown): T[] {
* @returns a list of pull request groups.
*/
export function getPullRequestGroups(
pullRequests: DashboardPullRequest[],
pullRequests: DashboardPullRequest[] | undefined,
configs: PullRequestGroupConfig[],
): PullRequestGroup[] {
): PullRequestGroup[] | undefined {
if (!pullRequests) {
return undefined;
}
const remainingPullRequests: DashboardPullRequest[] = [...pullRequests];
const pullRequestGroups: PullRequestGroup[] = [];
@@ -115,3 +107,17 @@ export function getPullRequestGroups(
return pullRequestGroups;
}
export function getPullRequestGroupConfigs(
columnConfigs: PullRequestColumnConfig[],
filterProcessor: (filters: Filter[]) => Filter[],
): PullRequestGroupConfig[] {
return columnConfigs.map(columnConfig => {
const filters = filterProcessor(columnConfig.filters);
return {
title: columnConfig.title,
filter: createFilter(filters),
simplified: columnConfig.simplified,
};
});
}
+15
View File
@@ -23,3 +23,18 @@ export {
} from './plugin';
export { AzurePullRequestsIcon } from './components/AzurePullRequestsIcon';
export { FilterType } from './components/PullRequestsPage';
export type {
PullRequestColumnConfig,
BaseFilter,
Filter,
PullRequestFilter,
AssignedToUserFilter,
CreatedByUserFilter,
AssignedToTeamFilter,
CreatedByTeamFilter,
AssignedToTeamsFilter,
CreatedByTeamsFilter,
AllFilter,
} from './components/PullRequestsPage';
@@ -0,0 +1,34 @@
/*
* 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.
*/
export function arrayHas<T>(arr: T[], value: T): boolean {
return new Set<T>(arr).has(value);
}
export function stringArrayHas(
arr: Array<string | undefined>,
value: string | undefined,
ignoreCase: boolean = false,
): boolean {
if (ignoreCase) {
return arrayHas(
arr.map(a => a?.toLocaleLowerCase('en-US')),
value?.toLocaleLowerCase('en-US'),
);
}
return arrayHas(arr, value);
}
@@ -0,0 +1,19 @@
/*
* 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.
*/
export function equalsIgnoreCase(str1: string, str2: string): boolean {
return str1.toLocaleLowerCase('en-US') === str2.toLocaleLowerCase('en-US');
}
+19
View File
@@ -0,0 +1,19 @@
/*
* 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.
*/
export * from './arrayHas';
export * from './equalsIgnoreCase';
export * from './getDurationFromDates';