Merge branch 'master' of https://github.com/backstage/backstage into marley/7678-pull-request-dashboard
Signed-off-by: Marley Powell <Marley.Powell@exclaimer.com>
This commit is contained in:
@@ -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
|
||||
<EntitySwitch>
|
||||
// ...
|
||||
<EntitySwitch.Case if={isAzureDevOpsAvailable}>
|
||||
// Set defaultLimit to the max number of builds you would like to be able to see
|
||||
// the default if not set is 10
|
||||
<EntityAzurePipelinesContent defaultLimit={25} />
|
||||
</EntitySwitch.Case>
|
||||
// ...
|
||||
</EntitySwitch>
|
||||
```
|
||||
|
||||
**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 = (
|
||||
<EntityLayout>
|
||||
// ...
|
||||
<EntityLayout.Route if={isAzureDevOpsAvailable} path="/pull-requests" title="Pull Requests">
|
||||
<EntityAzureReposContent defaultLimit={25} />
|
||||
</EntityLayout.Route>
|
||||
// ...
|
||||
</EntityLayout>
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
@@ -7,19 +7,12 @@
|
||||
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { SvgIconProps } from '@material-ui/core';
|
||||
|
||||
// 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<
|
||||
{
|
||||
azurePullRequestDashboard: RouteRef<undefined>;
|
||||
azurePipelinesEntityContent: RouteRef<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
export const azureDevOpsPlugin: BackstagePlugin<{}, {}>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AzurePullRequestsIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
@@ -40,6 +33,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)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
@@ -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",
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
import {
|
||||
DashboardPullRequest,
|
||||
PullRequest,
|
||||
PullRequestOptions,
|
||||
RepoBuild,
|
||||
RepoBuildOptions,
|
||||
Team,
|
||||
@@ -36,6 +38,12 @@ export interface AzureDevOpsApi {
|
||||
options?: RepoBuildOptions,
|
||||
): Promise<{ items: RepoBuild[] }>;
|
||||
|
||||
getPullRequests(
|
||||
projectName: string,
|
||||
repoName: string,
|
||||
options?: PullRequestOptions,
|
||||
): Promise<{ items: PullRequest[] }>;
|
||||
|
||||
getDashboardPullRequests(
|
||||
projectName: string,
|
||||
): Promise<DashboardPullRequest[]>;
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
import {
|
||||
DashboardPullRequest,
|
||||
PullRequest,
|
||||
PullRequestOptions,
|
||||
RepoBuild,
|
||||
RepoBuildOptions,
|
||||
Team,
|
||||
@@ -42,9 +44,35 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
|
||||
repoName: string,
|
||||
options?: RepoBuildOptions,
|
||||
): Promise<{ items: RepoBuild[] }> {
|
||||
const items = await this.get<RepoBuild[]>(
|
||||
`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<RepoBuild[]>(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<PullRequest[]>(urlSegment);
|
||||
return { items };
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
<SvgIcon {...props} viewBox="0 0 512 512">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="32"
|
||||
d="M461.81 53.81a4.4 4.4 0 00-3.3-3.39c-54.38-13.3-180 34.09-248.13 102.17a294.9 294.9 0 00-33.09 39.08c-21-1.9-42-.3-59.88 7.5-50.49 22.2-65.18 80.18-69.28 105.07a9 9 0 009.8 10.4l81.07-8.9a180.29 180.29 0 001.1 18.3 18.15 18.15 0 005.3 11.09l31.39 31.39a18.15 18.15 0 0011.1 5.3 179.91 179.91 0 0018.19 1.1l-8.89 81a9 9 0 0010.39 9.79c24.9-4 83-18.69 105.07-69.17 7.8-17.9 9.4-38.79 7.6-59.69a293.91 293.91 0 0039.19-33.09c68.38-68 115.47-190.86 102.37-247.95zM298.66 213.67a42.7 42.7 0 1160.38 0 42.65 42.65 0 01-60.38 0z"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="32"
|
||||
d="M109.64 352a45.06 45.06 0 00-26.35 12.84C65.67 382.52 64 448 64 448s65.52-1.67 83.15-19.31A44.73 44.73 0 00160 402.32"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
@@ -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';
|
||||
+44
-2
@@ -20,9 +20,51 @@ import React from 'react';
|
||||
|
||||
export const AzurePullRequestsIcon = (props: SvgIconProps) => (
|
||||
<SvgIcon {...props} viewBox="0 0 512 512">
|
||||
<circle
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="32"
|
||||
cx="128"
|
||||
cy="416"
|
||||
r="48"
|
||||
/>
|
||||
<path
|
||||
d="M160 95.1c0-44.18-35.82-80-80-80c-44.18 0-80 35.82-80 80C0 128.8 19.77 156.9 48 169.2V342.8C19.77 355.1 0 383.2 0 415.1c0 44.18 35.82 80 80 80c44.18 0 80-35.82 80-80c0-32.79-19.77-60.89-48-73.25V169.2C140.2 156.9 160 128.8 160 95.1zM80 439.1c-13.23 0-24-10.77-24-24s10.77-24 24-24c13.23 0 24 10.77 24 24S93.23 439.1 80 439.1zM80 119.1c-13.23 0-24-10.77-24-24s10.77-24 24-24c13.23 0 24 10.77 24 24S93.23 119.1 80 119.1zM464 342.8v-182.8c0-52.94-43.06-96-96-96H320v-48c0-6.219-3.594-11.84-9.219-14.5c-5.594-2.594-12.25-1.781-17.03 2.219l-96 80C194.1 86.75 192 91.25 192 95.1S194.1 105.2 197.8 108.3l96 80C296.7 190.7 300.3 191.1 304 191.1c2.312 0 4.625-.5 6.781-1.5C316.4 187.8 320 182.2 320 175.1v-48h48c17.66 0 32 14.34 32 32v182.8c-28.23 12.36-48 40.46-48 73.25c0 44.18 35.82 80 80 80c44.18 0 80-35.82 80-80C512 383.2 492.2 355.1 464 342.8zM432 439.1c-13.23 0-24-10.77-24-24s10.77-24 24-24c13.23 0 24 10.77 24 24S445.2 439.1 432 439.1z"
|
||||
fill="currentColor"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="32"
|
||||
d="M128 144v224M288 160l-64-64 64-64"
|
||||
/>
|
||||
<circle
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="32"
|
||||
cx="128"
|
||||
cy="96"
|
||||
r="48"
|
||||
/>
|
||||
<circle
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="32"
|
||||
cx="384"
|
||||
cy="416"
|
||||
r="48"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="32"
|
||||
d="M240 96h84a60 60 0 0160 60v212"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ const generateTestData = (rows = 10): RepoBuild[] => {
|
||||
title: `backstage ci - 1.0.0-preview-${rows - i}`,
|
||||
status,
|
||||
result,
|
||||
queueTime: new Date(Date.now() - i * 60000),
|
||||
queueTime: new Date(Date.now() - i * 60000).toISOString(),
|
||||
source: 'refs/heads/main',
|
||||
link: '',
|
||||
});
|
||||
|
||||
@@ -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,12 +146,27 @@ const columns: TableColumn[] = [
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Duration',
|
||||
field: 'queueTime',
|
||||
width: 'auto',
|
||||
render: (row: Partial<RepoBuild>) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Typography>
|
||||
{getDurationFromDates(row.startTime, row.finishTime)}
|
||||
</Typography>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
field: 'queueTime',
|
||||
width: 'auto',
|
||||
render: (row: Partial<RepoBuild>) =>
|
||||
DateTime.fromJSDate(row.queueTime ?? new Date()).toRelative(),
|
||||
(row.queueTime
|
||||
? DateTime.fromISO(row.queueTime)
|
||||
: DateTime.now()
|
||||
).toRelative(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -178,7 +195,13 @@ export const BuildTable = ({ items, loading, error }: BuildTableProps) => {
|
||||
pageSize: 5,
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title={`Builds (${items ? items.length : 0})`}
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<AzurePipelinesIcon style={{ fontSize: 30 }} />
|
||||
<Box mr={1} />
|
||||
Azure Pipelines - Builds ({items ? items.length : 0})
|
||||
</Box>
|
||||
}
|
||||
data={items ?? []}
|
||||
/>
|
||||
);
|
||||
|
||||
+26
@@ -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 <PullRequestTable defaultLimit={defaultLimit} />;
|
||||
};
|
||||
@@ -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';
|
||||
+57
@@ -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 (
|
||||
<ButtonGroup aria-label="outlined button group">
|
||||
<Button
|
||||
color={status === PullRequestStatus.Active ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setStatus(PullRequestStatus.Active);
|
||||
}}
|
||||
>
|
||||
Active
|
||||
</Button>
|
||||
<Button
|
||||
color={status === PullRequestStatus.Completed ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setStatus(PullRequestStatus.Completed);
|
||||
}}
|
||||
>
|
||||
Completed
|
||||
</Button>
|
||||
<Button
|
||||
color={status === PullRequestStatus.Abandoned ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setStatus(PullRequestStatus.Abandoned);
|
||||
}}
|
||||
>
|
||||
Abandoned
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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<PullRequest>) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Link to={row.link ?? ''}>{row.title}</Link>
|
||||
{row.isDraft && (
|
||||
<Box paddingLeft={1}>
|
||||
<Chip
|
||||
label="Draft"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
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<PullRequest>) =>
|
||||
(row.creationDate
|
||||
? DateTime.fromISO(row.creationDate)
|
||||
: DateTime.now()
|
||||
).toRelative(),
|
||||
},
|
||||
];
|
||||
|
||||
type PullRequestTableProps = {
|
||||
defaultLimit?: number;
|
||||
};
|
||||
|
||||
export const PullRequestTable = ({ defaultLimit }: PullRequestTableProps) => {
|
||||
const [pullRequestStatusState, setPullRequestStatusState] =
|
||||
useState<PullRequestStatus>(PullRequestStatus.Active);
|
||||
const { entity } = useEntity();
|
||||
|
||||
const { items, loading, error } = usePullRequests(
|
||||
entity,
|
||||
defaultLimit,
|
||||
pullRequestStatusState,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<ResponseErrorPanel error={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
isLoading={loading}
|
||||
columns={columns}
|
||||
options={{
|
||||
search: true,
|
||||
paging: true,
|
||||
pageSize: 5,
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<AzurePullRequestsIcon style={{ fontSize: 30 }} />
|
||||
<Box mr={1} />
|
||||
Azure Repos - Pull Requests ({items ? items.length : 0})
|
||||
<Box position="absolute" right={320} top={20}>
|
||||
<PullRequestStatusButtonGroup
|
||||
status={pullRequestStatusState}
|
||||
setStatus={setPullRequestStatusState}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
data={items ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
export {
|
||||
azureDevOpsPlugin,
|
||||
EntityAzurePipelinesContent,
|
||||
EntityAzurePullRequestsContent,
|
||||
isAzureDevOpsAvailable,
|
||||
AzurePullRequestsPage,
|
||||
} from './plugin';
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import {
|
||||
azurePipelinesEntityContentRouteRef,
|
||||
azurePullRequestDashboardRouteRef,
|
||||
azurePullRequestsEntityContentRouteRef,
|
||||
} from './routes';
|
||||
import {
|
||||
createApiFactory,
|
||||
@@ -44,10 +45,6 @@ export const azureDevOpsPlugin = createPlugin({
|
||||
new AzureDevOpsClient({ discoveryApi, identityApi }),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
azurePullRequestDashboard: azurePullRequestDashboardRouteRef,
|
||||
azurePipelinesEntityContent: azurePipelinesEntityContentRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
export const AzurePullRequestsPage = azureDevOpsPlugin.provide(
|
||||
@@ -69,3 +66,14 @@ export const EntityAzurePipelinesContent = azureDevOpsPlugin.provide(
|
||||
mountPoint: azurePipelinesEntityContentRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
export const EntityAzurePullRequestsContent = azureDevOpsPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'EntityAzurePullRequestsContent',
|
||||
component: () =>
|
||||
import('./components/EntityPageAzurePullRequests').then(
|
||||
m => m.EntityPageAzurePullRequests,
|
||||
),
|
||||
mountPoint: azurePullRequestsEntityContentRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -17,10 +17,14 @@
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const azurePullRequestDashboardRouteRef = createRouteRef({
|
||||
id: 'azure-pull-request-dashboard',
|
||||
path: '',
|
||||
title: 'Azure Pull Requests',
|
||||
});
|
||||
|
||||
export const azurePipelinesEntityContentRouteRef = createRouteRef({
|
||||
title: 'Azure Pipelines Entity Content',
|
||||
id: 'azure-pipelines-entity-content',
|
||||
});
|
||||
|
||||
export const azurePullRequestsEntityContentRouteRef = createRouteRef({
|
||||
id: 'azure-pull-requests-entity-content',
|
||||
});
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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: '',
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user