diff --git a/.changeset/new-cobras-fly.md b/.changeset/new-cobras-fly.md
new file mode 100644
index 0000000000..f39f1b0fbb
--- /dev/null
+++ b/.changeset/new-cobras-fly.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-azure-devops': patch
+---
+
+Added entity view for Azure Repo Pull Requests
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 3fe027ddad..3b9b381380 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -35,6 +35,7 @@ import {
} from '@backstage/plugin-api-docs';
import {
EntityAzurePipelinesContent,
+ EntityAzurePullRequestsContent,
isAzureDevOpsAvailable,
} from '@backstage/plugin-azure-devops';
import { EntityBadgesDialog } from '@backstage/plugin-badges';
@@ -264,6 +265,18 @@ const errorsContent = (
);
+const pullRequestsContent = (
+
+
+
+
+
+
+
+
+
+);
+
const overviewContent = (
{entityWarningContent}
@@ -368,7 +381,7 @@ const serviceEntityPage = (
-
+ {pullRequestsContent}
@@ -427,7 +440,7 @@ const websiteEntityPage = (
-
+ {pullRequestsContent}
diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md
index 47a0c4debd..f33bd3bdb6 100644
--- a/plugins/azure-devops/README.md
+++ b/plugins/azure-devops/README.md
@@ -6,10 +6,16 @@ Website: [https://dev.azure.com/](https://dev.azure.com/)
### Azure Pipelines
-Lists the top _n_ builds for a given repository where _n_ is a configurable value
+Lists the top _n_ builds for a given Azure Repo where _n_ is a configurable value

+### Azure Repos
+
+Lists the top _n_ Active, Completed, or Abandoned Pull Requests for a given repository where _n_ is a configurable value
+
+
+
## Setup
The following sections will help you get the Azure DevOps plugin setup and running
@@ -69,14 +75,55 @@ To get the Azure Pipelines component working you'll need to do the following two
// ...
- // Set defaultLimit to the max number of builds you would like to be able to see
- // the default if not set is 10
// ...
```
+**Notes:**
+
+- The `if` prop is optional on the `EntitySwitch.Case`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation
+- The `defaultLimit` proper on the `EntityAzurePipelinesContent` will set the max number of Builds you would like to see, if not set this will default to 10
+
+### Azure Repos Component
+
+To get the Azure Repos component working you'll need to do the following two steps:
+
+1. First we need to add the @backstage/plugin-azure-devops package to your frontend app:
+
+ ```bash
+ # From your Backstage root directory
+ cd packages/app
+ yarn add @backstage/plugin-azure-devops
+ ```
+
+2. Second we need to add the `EntityAzureReposContent` extension to the entity page in your app:
+
+ ```tsx
+ // In packages/app/src/components/catalog/EntityPage.tsx
+ import {
+ EntityAzureReposContent,
+ isAzureDevOpsAvailable,
+ } from '@backstage/plugin-azure-devops';
+
+ // For example in the Service section
+ const serviceEntityPage = (
+
+ // ...
+
+
+
+ // ...
+
+ ```
+
+**Notes:**
+
+- You'll need to add the `EntityLayout.Route` above from step 2 to all the entity sections you want to see Pull Requests in. For example if you wanted to see Pull Requests when looking at Website entities then you would need to add this to the `websiteEntityPage` section.
+- The `if` prop is optional on the `EntityLayout.Route`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation
+- The `defaultLimit` proper on the `EntityAzureReposContent` will set the max number of Pull Requests you would like to see, if not set this will default to 10
+
## Limitations
- Currently multiple organizations are not supported
diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md
index fbe28c179c..ab659534c1 100644
--- a/plugins/azure-devops/api-report.md
+++ b/plugins/azure-devops/api-report.md
@@ -7,17 +7,11 @@
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
-import { RouteRef } from '@backstage/core-plugin-api';
// 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)
-export const azureDevOpsPlugin: BackstagePlugin<
- {
- entityContent: RouteRef;
- },
- {}
->;
+export const azureDevOpsPlugin: BackstagePlugin<{}, {}>;
// Warning: (ae-missing-release-tag) "EntityAzurePipelinesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -28,6 +22,15 @@ export const EntityAzurePipelinesContent: ({
defaultLimit?: number | undefined;
}) => JSX.Element;
+// Warning: (ae-missing-release-tag) "EntityAzurePullRequestsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export const EntityAzurePullRequestsContent: ({
+ defaultLimit,
+}: {
+ defaultLimit?: number | undefined;
+}) => JSX.Element;
+
// 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)
diff --git a/plugins/azure-devops/docs/azure-devops-builds.png b/plugins/azure-devops/docs/azure-devops-builds.png
index 4788a185ed..6ff93dc7e4 100644
Binary files a/plugins/azure-devops/docs/azure-devops-builds.png and b/plugins/azure-devops/docs/azure-devops-builds.png differ
diff --git a/plugins/azure-devops/docs/azure-devops-pull-requests.png b/plugins/azure-devops/docs/azure-devops-pull-requests.png
new file mode 100644
index 0000000000..ce1b2a4267
Binary files /dev/null and b/plugins/azure-devops/docs/azure-devops-pull-requests.png differ
diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json
index 235304c9fa..3f1876b974 100644
--- a/plugins/azure-devops/package.json
+++ b/plugins/azure-devops/package.json
@@ -37,6 +37,7 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
+ "humanize-duration": "^3.27.0",
"luxon": "^2.0.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
diff --git a/plugins/azure-devops/src/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts
index 05377b0b7b..90d291b25d 100644
--- a/plugins/azure-devops/src/api/AzureDevOpsApi.ts
+++ b/plugins/azure-devops/src/api/AzureDevOpsApi.ts
@@ -15,6 +15,8 @@
*/
import {
+ PullRequest,
+ PullRequestOptions,
RepoBuild,
RepoBuildOptions,
} from '@backstage/plugin-azure-devops-common';
@@ -33,4 +35,10 @@ export interface AzureDevOpsApi {
repoName: string,
options?: RepoBuildOptions,
): Promise<{ items: RepoBuild[] }>;
+
+ getPullRequests(
+ projectName: string,
+ repoName: string,
+ options?: PullRequestOptions,
+ ): Promise<{ items: PullRequest[] }>;
}
diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts
index 4fd4e781c4..cbcc496725 100644
--- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts
+++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts
@@ -16,6 +16,8 @@
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import {
+ PullRequest,
+ PullRequestOptions,
RepoBuild,
RepoBuildOptions,
} from '@backstage/plugin-azure-devops-common';
@@ -35,14 +37,40 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
this.identityApi = options.identityApi;
}
- async getRepoBuilds(
+ public async getRepoBuilds(
projectName: string,
repoName: string,
options?: RepoBuildOptions,
): Promise<{ items: RepoBuild[] }> {
- const items = await this.get(
- `repo-builds/${projectName}/${repoName}?top=${options?.top}`,
- );
+ const queryString = new URLSearchParams();
+ if (options?.top) {
+ queryString.append('top', options.top.toString());
+ }
+ const urlSegment = `repo-builds/${encodeURIComponent(
+ projectName,
+ )}/${encodeURIComponent(repoName)}?${queryString}`;
+
+ const items = await this.get(urlSegment);
+ return { items };
+ }
+
+ public async getPullRequests(
+ projectName: string,
+ repoName: string,
+ options?: PullRequestOptions,
+ ): Promise<{ items: PullRequest[] }> {
+ const queryString = new URLSearchParams();
+ if (options?.top) {
+ queryString.append('top', options.top.toString());
+ }
+ if (options?.status) {
+ queryString.append('status', options.status.toString());
+ }
+ const urlSegment = `pull-requests/${encodeURIComponent(
+ projectName,
+ )}/${encodeURIComponent(repoName)}?${queryString}`;
+
+ const items = await this.get(urlSegment);
return { items };
}
diff --git a/plugins/azure-devops/src/components/AzurePipelinesIcon/AzurePipelinesIcon.tsx b/plugins/azure-devops/src/components/AzurePipelinesIcon/AzurePipelinesIcon.tsx
new file mode 100644
index 0000000000..bd19331f6e
--- /dev/null
+++ b/plugins/azure-devops/src/components/AzurePipelinesIcon/AzurePipelinesIcon.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { SvgIcon, SvgIconProps } from '@material-ui/core';
+
+import React from 'react';
+
+export const AzurePipelinesIcon = (props: SvgIconProps) => (
+
+
+
+
+);
diff --git a/plugins/azure-devops/src/components/AzurePipelinesIcon/index.ts b/plugins/azure-devops/src/components/AzurePipelinesIcon/index.ts
new file mode 100644
index 0000000000..4eb161f7bc
--- /dev/null
+++ b/plugins/azure-devops/src/components/AzurePipelinesIcon/index.ts
@@ -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 { AzurePipelinesIcon } from './AzurePipelinesIcon';
diff --git a/plugins/azure-devops/src/components/AzurePullRequestsIcon/AzurePullRequestsIcon.tsx b/plugins/azure-devops/src/components/AzurePullRequestsIcon/AzurePullRequestsIcon.tsx
new file mode 100644
index 0000000000..bb4cb8c081
--- /dev/null
+++ b/plugins/azure-devops/src/components/AzurePullRequestsIcon/AzurePullRequestsIcon.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 { SvgIcon, SvgIconProps } from '@material-ui/core';
+
+import React from 'react';
+
+export const AzurePullRequestsIcon = (props: SvgIconProps) => (
+
+
+
+
+
+
+
+);
diff --git a/plugins/azure-devops/src/components/AzurePullRequestsIcon/index.ts b/plugins/azure-devops/src/components/AzurePullRequestsIcon/index.ts
new file mode 100644
index 0000000000..1f49119b4b
--- /dev/null
+++ b/plugins/azure-devops/src/components/AzurePullRequestsIcon/index.ts
@@ -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 { AzurePullRequestsIcon } from './AzurePullRequestsIcon';
diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx
index c16c836393..463bf15d78 100644
--- a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx
+++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx
@@ -33,8 +33,10 @@ import {
TableColumn,
} from '@backstage/core-components';
+import { AzurePipelinesIcon } from '../AzurePipelinesIcon';
import { DateTime } from 'luxon';
import React from 'react';
+import { getDurationFromDates } from '../../utils/getDurationFromDates';
export const getBuildResultComponent = (result: number | undefined) => {
switch (result) {
@@ -144,6 +146,18 @@ const columns: TableColumn[] = [
),
},
+ {
+ title: 'Duration',
+ field: 'queueTime',
+ width: 'auto',
+ render: (row: Partial) => (
+
+
+ {getDurationFromDates(row.startTime, row.finishTime)}
+
+
+ ),
+ },
{
title: 'Age',
field: 'queueTime',
@@ -181,7 +195,13 @@ export const BuildTable = ({ items, loading, error }: BuildTableProps) => {
pageSize: 5,
showEmptyDataSourceMessage: !loading,
}}
- title={`Builds (${items ? items.length : 0})`}
+ title={
+
+
+
+ Azure Pipelines - Builds ({items ? items.length : 0})
+
+ }
data={items ?? []}
/>
);
diff --git a/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx b/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx
new file mode 100644
index 0000000000..3d1a3ef77a
--- /dev/null
+++ b/plugins/azure-devops/src/components/EntityPageAzurePullRequests/EntityPageAzurePullRequests.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 { PullRequestTable } from '../PullRequestTable/PullRequestTable';
+import React from 'react';
+
+export const EntityPageAzurePullRequests = ({
+ defaultLimit,
+}: {
+ defaultLimit?: number;
+}) => {
+ return ;
+};
diff --git a/plugins/azure-devops/src/components/EntityPageAzurePullRequests/index.ts b/plugins/azure-devops/src/components/EntityPageAzurePullRequests/index.ts
new file mode 100644
index 0000000000..2ec6c5bf04
--- /dev/null
+++ b/plugins/azure-devops/src/components/EntityPageAzurePullRequests/index.ts
@@ -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 { EntityPageAzurePullRequests } from './EntityPageAzurePullRequests';
diff --git a/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/PullRequestStatusButtonGroup.tsx b/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/PullRequestStatusButtonGroup.tsx
new file mode 100644
index 0000000000..f516bfb0e5
--- /dev/null
+++ b/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/PullRequestStatusButtonGroup.tsx
@@ -0,0 +1,57 @@
+/*
+ * 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 { Button, ButtonGroup } from '@material-ui/core';
+
+import { PullRequestStatus } from '@backstage/plugin-azure-devops-common';
+import React from 'react';
+
+export const PullRequestStatusButtonGroup = ({
+ status,
+ setStatus,
+}: {
+ status: PullRequestStatus;
+ setStatus: (pullRequestStatus: PullRequestStatus) => void;
+}) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/index.ts b/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/index.ts
new file mode 100644
index 0000000000..10a667803a
--- /dev/null
+++ b/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/index.ts
@@ -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 { PullRequestStatusButtonGroup } from './PullRequestStatusButtonGroup';
diff --git a/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx b/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx
new file mode 100644
index 0000000000..4365b796bc
--- /dev/null
+++ b/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx
@@ -0,0 +1,139 @@
+/*
+ * 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 { Box, Chip } from '@material-ui/core';
+import {
+ Link,
+ ResponseErrorPanel,
+ Table,
+ TableColumn,
+} from '@backstage/core-components';
+import {
+ PullRequest,
+ PullRequestStatus,
+} from '@backstage/plugin-azure-devops-common';
+import React, { useState } from 'react';
+
+import { AzurePullRequestsIcon } from '../AzurePullRequestsIcon';
+import { DateTime } from 'luxon';
+import { PullRequestStatusButtonGroup } from '../PullRequestStatusButtonGroup';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { usePullRequests } from '../../hooks/usePullRequests';
+
+const columns: TableColumn[] = [
+ {
+ title: 'ID',
+ field: 'pullRequestId',
+ highlight: false,
+ width: 'auto',
+ },
+ {
+ title: 'Title',
+ field: 'title',
+ width: 'auto',
+ render: (row: Partial) => (
+
+ {row.title}
+ {row.isDraft && (
+
+
+
+ )}
+
+ ),
+ },
+ {
+ title: 'Source',
+ field: 'sourceRefName',
+ width: 'auto',
+ },
+ {
+ title: 'Target',
+ field: 'targetRefName',
+ width: 'auto',
+ },
+ {
+ title: 'Created By',
+ field: 'createdBy',
+ width: 'auto',
+ },
+ {
+ title: 'Created',
+ field: 'creationDate',
+ width: 'auto',
+ render: (row: Partial) =>
+ (row.creationDate
+ ? DateTime.fromISO(row.creationDate)
+ : DateTime.now()
+ ).toRelative(),
+ },
+];
+
+type PullRequestTableProps = {
+ defaultLimit?: number;
+};
+
+export const PullRequestTable = ({ defaultLimit }: PullRequestTableProps) => {
+ const [pullRequestStatusState, setPullRequestStatusState] =
+ useState(PullRequestStatus.Active);
+ const { entity } = useEntity();
+
+ const { items, loading, error } = usePullRequests(
+ entity,
+ defaultLimit,
+ pullRequestStatusState,
+ );
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Azure Repos - Pull Requests ({items ? items.length : 0})
+
+
+
+
+ }
+ data={items ?? []}
+ />
+ );
+};
diff --git a/plugins/azure-devops/src/components/PullRequestTable/index.ts b/plugins/azure-devops/src/components/PullRequestTable/index.ts
new file mode 100644
index 0000000000..956e8afa2d
--- /dev/null
+++ b/plugins/azure-devops/src/components/PullRequestTable/index.ts
@@ -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 { PullRequestTable } from './PullRequestTable';
diff --git a/plugins/azure-devops/src/hooks/usePullRequests.ts b/plugins/azure-devops/src/hooks/usePullRequests.ts
new file mode 100644
index 0000000000..178bf0495f
--- /dev/null
+++ b/plugins/azure-devops/src/hooks/usePullRequests.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 {
+ PullRequest,
+ PullRequestOptions,
+ PullRequestStatus,
+} from '@backstage/plugin-azure-devops-common';
+
+import { AZURE_DEVOPS_DEFAULT_TOP } from '../constants';
+import { Entity } from '@backstage/catalog-model';
+import { azureDevOpsApiRef } from '../api';
+import { useApi } from '@backstage/core-plugin-api';
+import { useAsync } from 'react-use';
+import { useProjectRepoFromEntity } from './useProjectRepoFromEntity';
+
+export function usePullRequests(
+ entity: Entity,
+ defaultLimit?: number,
+ requestedStatus?: PullRequestStatus,
+): {
+ items?: PullRequest[];
+ loading: boolean;
+ error?: Error;
+} {
+ const top = defaultLimit ?? AZURE_DEVOPS_DEFAULT_TOP;
+ const status = requestedStatus ?? PullRequestStatus.Active;
+ const options: PullRequestOptions = {
+ top,
+ status,
+ };
+
+ const api = useApi(azureDevOpsApiRef);
+ const { project, repo } = useProjectRepoFromEntity(entity);
+
+ const { value, loading, error } = useAsync(() => {
+ return api.getPullRequests(project, repo, options);
+ }, [api, project, repo, top, status]);
+
+ return {
+ items: value?.items,
+ loading,
+ error,
+ };
+}
diff --git a/plugins/azure-devops/src/index.ts b/plugins/azure-devops/src/index.ts
index 71d3b654fe..4da533b534 100644
--- a/plugins/azure-devops/src/index.ts
+++ b/plugins/azure-devops/src/index.ts
@@ -16,5 +16,6 @@
export {
azureDevOpsPlugin,
EntityAzurePipelinesContent,
+ EntityAzurePullRequestsContent,
isAzureDevOpsAvailable,
} from './plugin';
diff --git a/plugins/azure-devops/src/plugin.ts b/plugins/azure-devops/src/plugin.ts
index ab6d816460..7a5f80d0d3 100644
--- a/plugins/azure-devops/src/plugin.ts
+++ b/plugins/azure-devops/src/plugin.ts
@@ -14,6 +14,10 @@
* limitations under the License.
*/
+import {
+ azurePipelinesEntityContentRouteRef,
+ azurePullRequestsEntityContentRouteRef,
+} from './routes';
import {
createApiFactory,
createPlugin,
@@ -26,7 +30,6 @@ import { AZURE_DEVOPS_ANNOTATION } from './constants';
import { AzureDevOpsClient } from './api/AzureDevOpsClient';
import { Entity } from '@backstage/catalog-model';
import { azureDevOpsApiRef } from './api/AzureDevOpsApi';
-import { azureDevOpsRouteRef } from './routes';
export const isAzureDevOpsAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION]);
@@ -41,9 +44,6 @@ export const azureDevOpsPlugin = createPlugin({
new AzureDevOpsClient({ discoveryApi, identityApi }),
}),
],
- routes: {
- entityContent: azureDevOpsRouteRef,
- },
});
export const EntityAzurePipelinesContent = azureDevOpsPlugin.provide(
@@ -53,6 +53,17 @@ export const EntityAzurePipelinesContent = azureDevOpsPlugin.provide(
import('./components/EntityPageAzurePipelines').then(
m => m.EntityPageAzurePipelines,
),
- mountPoint: azureDevOpsRouteRef,
+ mountPoint: azurePipelinesEntityContentRouteRef,
+ }),
+);
+
+export const EntityAzurePullRequestsContent = azureDevOpsPlugin.provide(
+ createRoutableExtension({
+ name: 'EntityAzurePullRequestsContent',
+ component: () =>
+ import('./components/EntityPageAzurePullRequests').then(
+ m => m.EntityPageAzurePullRequests,
+ ),
+ mountPoint: azurePullRequestsEntityContentRouteRef,
}),
);
diff --git a/plugins/azure-devops/src/routes.ts b/plugins/azure-devops/src/routes.ts
index 81f4b48fda..7a4adea33e 100644
--- a/plugins/azure-devops/src/routes.ts
+++ b/plugins/azure-devops/src/routes.ts
@@ -13,8 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { createRouteRef } from '@backstage/core-plugin-api';
-export const azureDevOpsRouteRef = createRouteRef({
- title: 'azure-devops',
+export const azurePipelinesEntityContentRouteRef = createRouteRef({
+ id: 'azure-pipelines-entity-content',
+});
+
+export const azurePullRequestsEntityContentRouteRef = createRouteRef({
+ id: 'azure-pull-requests-entity-content',
});
diff --git a/plugins/azure-devops/src/utils/getDurationFromDates.test.ts b/plugins/azure-devops/src/utils/getDurationFromDates.test.ts
new file mode 100644
index 0000000000..fac0e917d0
--- /dev/null
+++ b/plugins/azure-devops/src/utils/getDurationFromDates.test.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { getDurationFromDates } from './getDurationFromDates';
+
+describe('getDurationFromDates', () => {
+ beforeAll(() => {
+ jest.useFakeTimers('modern');
+ jest.setSystemTime(new Date('2021-10-15T09:45:25.0000000Z'));
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ describe('getDurationFromDates with valid startTime and valid finishTime', () => {
+ it('should return empty result', () => {
+ const startTime = '2021-10-15T09:30:00.0000000Z';
+ const finishTime = '2021-10-15T11:00:00.0000000Z';
+
+ const result = getDurationFromDates(startTime, finishTime);
+
+ expect(result).toEqual('1h, 30m');
+ });
+ });
+
+ describe('getDurationFromDates with valid startTime and undefined finishTime', () => {
+ it('should return empty result', () => {
+ const startTime = '2021-10-15T09:30:00.0000000Z';
+
+ const result = getDurationFromDates(startTime, undefined);
+
+ expect(result).toEqual('15m, 25s');
+ });
+ });
+
+ describe('getDurationFromDates with undefined startTime and valid finishTime', () => {
+ it('should return empty result', () => {
+ const finishTime = '2021-10-15T11:00:00.0000000Z';
+
+ const result = getDurationFromDates(undefined, finishTime);
+
+ expect(result).toEqual('');
+ });
+ });
+
+ describe('getDurationFromDates with undefined startTime and undefined finishTime', () => {
+ it('should return empty result', () => {
+ const result = getDurationFromDates(undefined, undefined);
+
+ expect(result).toEqual('');
+ });
+ });
+});
diff --git a/plugins/azure-devops/src/utils/getDurationFromDates.ts b/plugins/azure-devops/src/utils/getDurationFromDates.ts
new file mode 100644
index 0000000000..789849f975
--- /dev/null
+++ b/plugins/azure-devops/src/utils/getDurationFromDates.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { DateTime, Interval } from 'luxon';
+import humanizeDuration from 'humanize-duration';
+
+export const getDurationFromDates = (
+ startTime?: string,
+ finishTime?: string,
+): string => {
+ if (!startTime || (!startTime && !finishTime)) {
+ return '';
+ }
+
+ const start = DateTime.fromISO(startTime);
+ const finish = finishTime ? DateTime.fromISO(finishTime) : DateTime.now();
+
+ const formatted = Interval.fromDateTimes(start, finish)
+ .toDuration()
+ .valueOf();
+
+ const shortEnglishHumanizer = humanizeDuration.humanizer({
+ language: 'shortEn',
+ languages: {
+ shortEn: {
+ y: () => 'y',
+ mo: () => 'mo',
+ w: () => 'w',
+ d: () => 'd',
+ h: () => 'h',
+ m: () => 'm',
+ s: () => 's',
+ ms: () => 'ms',
+ },
+ },
+ });
+
+ return shortEnglishHumanizer(formatted, {
+ largest: 2,
+ round: true,
+ spacer: '',
+ });
+};