@backstage-community/plugin-bitbucket-pull-requests 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @backstage-community/plugin-bitbucket-pull-requests
2
+
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - fbd92a7: move plugin to the community-plugins backstage repo
8
+
9
+ ### Minor Changes
10
+
11
+ - d778269: Add a new bitbucket homepage component
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Bitbucket PullRequest Plugin for Backstage
2
+
3
+ ![list of pull requests in the Bitbucket repo](./docs/bitbucketprimg.png)
4
+
5
+ ## Features
6
+
7
+ - List of PR's from particular bitbucket repo
8
+ - Filtering like OPEN/CLOSED/MERGED/ALL PR and Search
9
+ - Able to view Creator name, Created date and last update etc.
10
+ - We can go to Particular PR by clicking ID.
11
+
12
+ ## Limitations
13
+
14
+ This plugin currently only works with Bitbucket Data center. Bitbucket cloud uses a different API to get pull requests [documented here](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-get). Contributions are welcome to add support for Bitbucket cloud.
15
+
16
+ ## How to add Bitbucket PR plugin to Backstage app
17
+
18
+ 1. Install the plugin into Backstage.
19
+
20
+ ```bash
21
+ cd packages/app
22
+ yarn add @backstage-community/plugin-bitbucket-pull-requests
23
+ ```
24
+
25
+ 2. Add plugin API to your Backstage instance.
26
+
27
+ ```ts
28
+ // packages/app/src/components/catalog/EntityPage.tsx
29
+ import { EntityBitbucketPullRequestsContent } from '@backstage-community/plugin-bitbucket-pull-requests';
30
+
31
+ ...
32
+
33
+ const serviceEntityPage = (
34
+ <EntityLayout>
35
+ ...
36
+ <EntityLayout.Route path="/bitbucket-pullrequests" title="Bitbucket">
37
+ <EntityBitbucketPullRequestsContent />
38
+ </EntityLayout.Route>
39
+ ...
40
+ </EntityLayout>
41
+ ```
42
+
43
+ 3. Add proxy config
44
+
45
+ ```yaml
46
+ // app-config.yaml
47
+ proxy:
48
+ '/bitbucket/api':
49
+ target: https://bitbucket.org
50
+ changeOrigin: true
51
+ headers:
52
+ Authorization: Bearer ${BITBUCKET_TOKEN}
53
+ Accept: 'application/json'
54
+ Content-Type: 'application/json'
55
+
56
+ bitbucket:
57
+ # Defaults to /bitbucket/api and can be omitted if proxy is configured for that url
58
+ proxyPath: /bitbucket/api
59
+ ```
60
+
61
+ 4. Run backstage app with `yarn start` and navigate to services tabs.
62
+
63
+ ## How to use Bitbucket PR plugin in Backstage
64
+
65
+ - Add annotation to the yaml config file of a component
66
+
67
+ ```yaml
68
+ metadata:
69
+ annotations:
70
+ bitbucket.com/project-slug: <example-bitbucket-project-name>/<example-bitbucket-repo-name>
71
+ ```
72
+
73
+ ## Adding Bitbucket Pull Requests to your Homepage
74
+
75
+ This plugin also provides a homepage component that displays your Bitbucket pull requests directly on your Backstage homepage.
76
+
77
+ ![Bitbucket homepage component](./docs/bitbucket_homepage.png)
78
+
79
+ ### Features
80
+
81
+ - View pull requests assigned to you or authored by you in a tabbed interface
82
+ - Shows PR ID, title, repository, branch, author/reviewers information, and build status (can be disabled)
83
+
84
+ ### How to add to your Homepage
85
+
86
+ 1. Make sure you've installed this plugin and configured the proxy as described above.
87
+
88
+ 2. Make sure you've installed the homepage plugin and configured it as described in the [official documentation](https://github.com/backstage/backstage/tree/master/plugins/home#readme).
89
+
90
+ 3. Add the component to your homepage:
91
+
92
+ ```tsx
93
+ // packages/app/src/components/home/HomePage.tsx
94
+ import { HomePagePullRequestsCard } from '@backstage-community/plugin-bitbucket-pull-requests';
95
+
96
+ export const HomePage = () => (
97
+ <Page themeId="home">
98
+ <Content>
99
+ <Grid container spacing={3}>
100
+ {/* Other homepage components */}
101
+ <Grid item xs={12} md={6}>
102
+ {/* Default: includes build status column */}
103
+ <HomePagePullRequestsCard />
104
+
105
+ {/* Or disable the build status column */}
106
+ {/* <HomePagePullRequestsCard buildStatus={false} /> */}
107
+ </Grid>
108
+ </Grid>
109
+ </Content>
110
+ </Page>
111
+ );
112
+ ```
113
+
114
+ ### Props
115
+
116
+ | Prop | Type | Default | Description |
117
+ | ------------- | ------- | ------- | ---------------------------------------------------------------------- |
118
+ | `buildStatus` | boolean | `true` | Whether to show build status column and fetch build status information |
@@ -0,0 +1,153 @@
1
+ import fetch from 'cross-fetch';
2
+ import { createApiRef } from '@backstage/core-plugin-api';
3
+ import { parseEntityRef } from '@backstage/catalog-model';
4
+
5
+ const bitbucketApiRef = createApiRef({
6
+ id: "plugin.bitbucket.service"
7
+ });
8
+ const DEFAULT_PROXY_PATH = "/bitbucket/api";
9
+ const DEFAULT_LIMIT = 50;
10
+ class BitbucketApi {
11
+ discoveryApi;
12
+ identityApi;
13
+ configApi;
14
+ constructor(options) {
15
+ this.discoveryApi = options.discoveryApi;
16
+ this.identityApi = options.identityApi;
17
+ this.configApi = options.configApi;
18
+ }
19
+ /**
20
+ * Gets the configured proxy path from config or returns the default
21
+ * @returns The configured proxy path
22
+ */
23
+ getProxyPath() {
24
+ return this.configApi?.getOptionalString("bitbucket.proxyPath") || DEFAULT_PROXY_PATH;
25
+ }
26
+ async fetchPullRequestListForRepo(project, repo, state, limit = DEFAULT_LIMIT) {
27
+ const proxyUrl = await this.discoveryApi.getBaseUrl("proxy");
28
+ const url = new URL(
29
+ `${proxyUrl}${this.getProxyPath()}/projects/${project}/repos/${repo}/pull-requests`
30
+ );
31
+ const params = new URLSearchParams();
32
+ if (state) {
33
+ params.append("state", state);
34
+ }
35
+ params.append("limit", limit.toString());
36
+ const response = await fetch(`${url}?${params}`, {
37
+ headers: {
38
+ "Content-Type": "application/json"
39
+ }
40
+ });
41
+ if (!response.ok) {
42
+ throw new Error("Failed to fetch pull requests");
43
+ }
44
+ const data = await response.json();
45
+ return this.mapPullRequests(data);
46
+ }
47
+ async fetchBuildStatus(commitId) {
48
+ const proxyUrl = await this.discoveryApi.getBaseUrl("proxy");
49
+ const response = await fetch(
50
+ `${proxyUrl}${this.getProxyPath()}/rest/build-status/latest/commits/stats/${commitId}`,
51
+ { headers: { "Content-Type": "application/json" } }
52
+ );
53
+ if (!response.ok) {
54
+ throw new Error(`Failed to fetch build status for commit ${commitId}`);
55
+ }
56
+ return response.json();
57
+ }
58
+ determineBuildState(status) {
59
+ if (status.failed > 0) return "FAILED";
60
+ if (status.inProgress > 0) return "INPROGRESS";
61
+ if (status.cancelled > 0) return "STOPPED";
62
+ if (status.successful > 0) return "SUCCESSFUL";
63
+ return void 0;
64
+ }
65
+ async enhancePrWithBuildStatus(pr) {
66
+ if (pr.latestCommit) {
67
+ return this.fetchBuildStatus(pr.latestCommit).then((buildStatus) => ({
68
+ ...pr,
69
+ buildStatus: this.determineBuildState(buildStatus)
70
+ })).catch(() => ({
71
+ ...pr,
72
+ buildStatus: "UNKNOWN"
73
+ }));
74
+ }
75
+ return pr;
76
+ }
77
+ mapPullRequests(data) {
78
+ return data.values?.map((pr) => ({
79
+ id: pr.id,
80
+ title: pr.title,
81
+ author: {
82
+ displayName: pr.author.user.displayName,
83
+ slug: pr.author.user.slug
84
+ },
85
+ createdDate: pr.createdDate,
86
+ updatedDate: pr.updatedDate,
87
+ state: pr.state,
88
+ url: pr.links.self[0].href,
89
+ repoUrl: pr.fromRef.repository.links.self[0].href,
90
+ description: pr.description || "",
91
+ fromRepo: pr.fromRef.repository.name,
92
+ fromProject: pr.fromRef.repository.project.key,
93
+ sourceBranch: pr.fromRef.displayId,
94
+ targetBranch: pr.toRef.displayId,
95
+ latestCommit: pr.fromRef.latestCommit,
96
+ reviewers: pr.reviewers?.map((r) => ({
97
+ displayName: r.user.displayName,
98
+ slug: r.user.slug
99
+ })) || []
100
+ })) || [];
101
+ }
102
+ async fetchUserPullRequests(role = "REVIEWER", state = "OPEN", limit = DEFAULT_LIMIT, options = { includeBuildStatus: true }) {
103
+ if (!this.identityApi) {
104
+ throw new Error("Identity API is not available");
105
+ }
106
+ const { userEntityRef } = await this.identityApi.getBackstageIdentity();
107
+ const { name } = parseEntityRef(userEntityRef);
108
+ if (!name) {
109
+ throw new Error("User not found");
110
+ }
111
+ const proxyUrl = await this.discoveryApi.getBaseUrl("proxy");
112
+ const url = new URL(
113
+ `${proxyUrl}${this.getProxyPath()}/dashboard/pull-requests`
114
+ );
115
+ const params = new URLSearchParams({
116
+ order: "participant_status",
117
+ limit: limit.toString(),
118
+ state,
119
+ role,
120
+ user: name
121
+ });
122
+ const response = await fetch(`${url}?${params}`, {
123
+ headers: {
124
+ "Content-Type": "application/json"
125
+ }
126
+ });
127
+ if (!response.ok) {
128
+ let errorMessage = "Failed to fetch pull requests from Bitbucket";
129
+ try {
130
+ const errorText = await response.text();
131
+ const errorJson = JSON.parse(errorText);
132
+ if (response.status === 404 && errorJson.errors?.[0]?.message?.includes("does not exist")) {
133
+ errorMessage = `User '${name}' not found in Bitbucket. Please ensure your Bitbucket account exists.`;
134
+ }
135
+ } catch (e) {
136
+ errorMessage = e instanceof Error ? e.message : String(e);
137
+ }
138
+ throw new Error(errorMessage);
139
+ }
140
+ const data = await response.json();
141
+ const pullRequests = this.mapPullRequests(data);
142
+ if (options.includeBuildStatus) {
143
+ const enhancedPullRequests = await Promise.all(
144
+ pullRequests.map((pr) => this.enhancePrWithBuildStatus(pr))
145
+ );
146
+ return enhancedPullRequests;
147
+ }
148
+ return pullRequests;
149
+ }
150
+ }
151
+
152
+ export { BitbucketApi, bitbucketApiRef };
153
+ //# sourceMappingURL=BitbucketApi.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BitbucketApi.esm.js","sources":["../../src/api/BitbucketApi.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport fetch from 'cross-fetch';\n\nimport {\n createApiRef,\n DiscoveryApi,\n IdentityApi,\n ConfigApi,\n} from '@backstage/core-plugin-api';\nimport { parseEntityRef } from '@backstage/catalog-model';\n\nexport const bitbucketApiRef = createApiRef<BitbucketApi>({\n id: 'plugin.bitbucket.service',\n});\n\nexport type User = {\n displayName: string;\n slug: string;\n};\n\nexport type BuildStatus = {\n cancelled: number;\n failed: number;\n inProgress: number;\n successful: number;\n unknown: number;\n};\n\nexport type PullRequest = {\n id: number;\n title: string;\n author: User;\n createdDate: number;\n updatedDate: number;\n state: string;\n description: string;\n url: string;\n repoUrl: string;\n fromRepo: string;\n fromProject: string;\n sourceBranch: string;\n targetBranch: string;\n latestCommit?: string;\n buildStatus?: 'SUCCESSFUL' | 'FAILED' | 'INPROGRESS' | 'STOPPED' | 'UNKNOWN';\n reviewers: User[];\n};\nconst DEFAULT_PROXY_PATH = '/bitbucket/api';\nconst DEFAULT_LIMIT = 50;\ntype Options = {\n discoveryApi: DiscoveryApi;\n identityApi: IdentityApi;\n configApi?: ConfigApi;\n};\nexport class BitbucketApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly identityApi: IdentityApi;\n private readonly configApi?: ConfigApi;\n\n constructor(options: Options) {\n this.discoveryApi = options.discoveryApi;\n this.identityApi = options.identityApi;\n this.configApi = options.configApi;\n }\n\n /**\n * Gets the configured proxy path from config or returns the default\n * @returns The configured proxy path\n */\n private getProxyPath(): string {\n return (\n this.configApi?.getOptionalString('bitbucket.proxyPath') ||\n DEFAULT_PROXY_PATH\n );\n }\n\n async fetchPullRequestListForRepo(\n project: string,\n repo: string,\n state?: string,\n limit: number = DEFAULT_LIMIT,\n ): Promise<PullRequest[]> {\n const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');\n const url = new URL(\n `${proxyUrl}${this.getProxyPath()}/projects/${project}/repos/${repo}/pull-requests`,\n );\n\n const params = new URLSearchParams();\n if (state) {\n params.append('state', state);\n }\n params.append('limit', limit.toString());\n\n const response = await fetch(`${url}?${params}`, {\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n if (!response.ok) {\n throw new Error('Failed to fetch pull requests');\n }\n\n const data = await response.json();\n return this.mapPullRequests(data);\n }\n\n private async fetchBuildStatus(commitId: string): Promise<BuildStatus> {\n const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');\n const response = await fetch(\n `${proxyUrl}${this.getProxyPath()}/rest/build-status/latest/commits/stats/${commitId}`,\n { headers: { 'Content-Type': 'application/json' } },\n );\n\n if (!response.ok) {\n throw new Error(`Failed to fetch build status for commit ${commitId}`);\n }\n\n return response.json();\n }\n\n private determineBuildState(\n status: BuildStatus,\n ): 'SUCCESSFUL' | 'FAILED' | 'INPROGRESS' | 'STOPPED' | undefined {\n if (status.failed > 0) return 'FAILED';\n if (status.inProgress > 0) return 'INPROGRESS';\n if (status.cancelled > 0) return 'STOPPED';\n if (status.successful > 0) return 'SUCCESSFUL';\n return undefined;\n }\n\n private async enhancePrWithBuildStatus(\n pr: PullRequest,\n ): Promise<PullRequest> {\n if (pr.latestCommit) {\n return this.fetchBuildStatus(pr.latestCommit)\n .then(buildStatus => ({\n ...pr,\n buildStatus: this.determineBuildState(buildStatus),\n }))\n .catch(() => ({\n ...pr,\n buildStatus: 'UNKNOWN' as const,\n }));\n }\n return pr;\n }\n\n public mapPullRequests(data: any): PullRequest[] {\n return (\n data.values?.map((pr: any) => ({\n id: pr.id,\n title: pr.title,\n author: {\n displayName: pr.author.user.displayName,\n slug: pr.author.user.slug,\n },\n createdDate: pr.createdDate,\n updatedDate: pr.updatedDate,\n state: pr.state,\n url: pr.links.self[0].href,\n repoUrl: pr.fromRef.repository.links.self[0].href,\n description: pr.description || '',\n fromRepo: pr.fromRef.repository.name,\n fromProject: pr.fromRef.repository.project.key,\n sourceBranch: pr.fromRef.displayId,\n targetBranch: pr.toRef.displayId,\n latestCommit: pr.fromRef.latestCommit,\n reviewers:\n pr.reviewers?.map((r: any) => ({\n displayName: r.user.displayName,\n slug: r.user.slug,\n })) || [],\n })) || []\n );\n }\n\n async fetchUserPullRequests(\n role: 'REVIEWER' | 'AUTHOR' = 'REVIEWER',\n state: 'OPEN' | 'MERGED' | 'DECLINED' | 'ALL' = 'OPEN',\n limit: number = DEFAULT_LIMIT,\n options: { includeBuildStatus?: boolean } = { includeBuildStatus: true },\n ): Promise<PullRequest[]> {\n if (!this.identityApi) {\n throw new Error('Identity API is not available');\n }\n\n const { userEntityRef } = await this.identityApi.getBackstageIdentity();\n\n const { name } = parseEntityRef(userEntityRef);\n\n if (!name) {\n throw new Error('User not found');\n }\n\n const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');\n const url = new URL(\n `${proxyUrl}${this.getProxyPath()}/dashboard/pull-requests`,\n );\n\n const params = new URLSearchParams({\n order: 'participant_status',\n limit: limit.toString(),\n state,\n role,\n user: name,\n });\n\n const response = await fetch(`${url}?${params}`, {\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n let errorMessage = 'Failed to fetch pull requests from Bitbucket';\n\n try {\n const errorText = await response.text();\n const errorJson = JSON.parse(errorText);\n\n if (\n response.status === 404 &&\n errorJson.errors?.[0]?.message?.includes('does not exist')\n ) {\n errorMessage = `User '${name}' not found in Bitbucket. Please ensure your Bitbucket account exists.`;\n }\n } catch (e) {\n errorMessage = e instanceof Error ? e.message : String(e);\n }\n\n throw new Error(errorMessage);\n }\n\n const data = await response.json();\n const pullRequests = this.mapPullRequests(data);\n if (options.includeBuildStatus) {\n const enhancedPullRequests = await Promise.all(\n pullRequests.map(pr => this.enhancePrWithBuildStatus(pr)),\n );\n return enhancedPullRequests;\n }\n return pullRequests;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useState } from 'react';
3
+ import { Box, Tabs, Tab } from '@material-ui/core';
4
+ import { HomePagePullRequestsTable } from './HomePagePullRequestsTable.esm.js';
5
+
6
+ function TabPanel(props) {
7
+ const { children, value, index, ...other } = props;
8
+ return /* @__PURE__ */ jsx(
9
+ "div",
10
+ {
11
+ role: "tabpanel",
12
+ hidden: value !== index,
13
+ id: `pr-tabpanel-${index}`,
14
+ "aria-labelledby": `pr-tab-${index}`,
15
+ ...other,
16
+ children: value === index && /* @__PURE__ */ jsx(Box, { children })
17
+ }
18
+ );
19
+ }
20
+ function a11yProps(index) {
21
+ return {
22
+ id: `pr-tab-${index}`,
23
+ "aria-controls": `pr-tabpanel-${index}`
24
+ };
25
+ }
26
+ const HomePagePullRequestsCard = ({
27
+ buildStatus = true
28
+ } = {}) => {
29
+ const [value, setValue] = useState(0);
30
+ const handleChange = (_event, newValue) => {
31
+ setValue(newValue);
32
+ };
33
+ return /* @__PURE__ */ jsxs(Box, { children: [
34
+ /* @__PURE__ */ jsxs(
35
+ Tabs,
36
+ {
37
+ value,
38
+ onChange: handleChange,
39
+ "aria-label": "Pull Requests tabs",
40
+ children: [
41
+ /* @__PURE__ */ jsx(Tab, { label: "Authored by Me", ...a11yProps(0) }),
42
+ /* @__PURE__ */ jsx(Tab, { label: "Assigned to Me", ...a11yProps(1) })
43
+ ]
44
+ }
45
+ ),
46
+ /* @__PURE__ */ jsx(TabPanel, { value, index: 0, children: /* @__PURE__ */ jsx(
47
+ HomePagePullRequestsTable,
48
+ {
49
+ userRole: "AUTHOR",
50
+ buildStatus
51
+ }
52
+ ) }),
53
+ /* @__PURE__ */ jsx(TabPanel, { value, index: 1, children: /* @__PURE__ */ jsx(
54
+ HomePagePullRequestsTable,
55
+ {
56
+ userRole: "REVIEWER",
57
+ buildStatus
58
+ }
59
+ ) })
60
+ ] });
61
+ };
62
+
63
+ export { HomePagePullRequestsCard };
64
+ //# sourceMappingURL=HomePagePullRequestsCard.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HomePagePullRequestsCard.esm.js","sources":["../../../src/components/HomePage/HomePagePullRequestsCard.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { useState } from 'react';\nimport { Tabs, Tab, Box } from '@material-ui/core';\nimport { HomePagePullRequestsTable } from './HomePagePullRequestsTable';\n\ninterface TabPanelProps {\n children?: React.ReactNode;\n index: number;\n value: number;\n}\n\nfunction TabPanel(props: Readonly<TabPanelProps>) {\n const { children, value, index, ...other } = props;\n\n return (\n <div\n role=\"tabpanel\"\n hidden={value !== index}\n id={`pr-tabpanel-${index}`}\n aria-labelledby={`pr-tab-${index}`}\n {...other}\n >\n {value === index && <Box>{children}</Box>}\n </div>\n );\n}\n\nfunction a11yProps(index: number) {\n return {\n id: `pr-tab-${index}`,\n 'aria-controls': `pr-tabpanel-${index}`,\n };\n}\n\n/**\n * Props for the HomePagePullRequestsCard component\n *\n * @public\n */\nexport interface HomePagePullRequestsCardProps {\n /**\n * Flag to determine whether to display build status information in the pull requests table.\n * Defaults to true if not specified.\n */\n buildStatus?: boolean;\n}\n\n/**\n * Component to display pull requests as tabs on the homepage\n *\n * @public\n */\nexport const HomePagePullRequestsCard = ({\n buildStatus = true,\n}: HomePagePullRequestsCardProps = {}) => {\n const [value, setValue] = useState(0);\n\n const handleChange = (\n _event: globalThis.React.ChangeEvent<{}>,\n newValue: number,\n ) => {\n setValue(newValue);\n };\n\n return (\n <Box>\n <Tabs\n value={value}\n onChange={handleChange}\n aria-label=\"Pull Requests tabs\"\n >\n <Tab label=\"Authored by Me\" {...a11yProps(0)} />\n <Tab label=\"Assigned to Me\" {...a11yProps(1)} />\n </Tabs>\n <TabPanel value={value} index={0}>\n <HomePagePullRequestsTable\n userRole=\"AUTHOR\"\n buildStatus={buildStatus}\n />\n </TabPanel>\n <TabPanel value={value} index={1}>\n <HomePagePullRequestsTable\n userRole=\"REVIEWER\"\n buildStatus={buildStatus}\n />\n </TabPanel>\n </Box>\n );\n};\n"],"names":[],"mappings":";;;;;AAyBA,SAAS,SAAS,KAAA,EAAgC;AAChD,EAAA,MAAM,EAAE,QAAA,EAAU,KAAA,EAAO,KAAA,EAAO,GAAG,OAAM,GAAI,KAAA;AAE7C,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,UAAA;AAAA,MACL,QAAQ,KAAA,KAAU,KAAA;AAAA,MAClB,EAAA,EAAI,eAAe,KAAK,CAAA,CAAA;AAAA,MACxB,iBAAA,EAAiB,UAAU,KAAK,CAAA,CAAA;AAAA,MAC/B,GAAG,KAAA;AAAA,MAEH,QAAA,EAAA,KAAA,KAAU,KAAA,oBAAS,GAAA,CAAC,GAAA,EAAA,EAAK,QAAA,EAAS;AAAA;AAAA,GACrC;AAEJ;AAEA,SAAS,UAAU,KAAA,EAAe;AAChC,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,UAAU,KAAK,CAAA,CAAA;AAAA,IACnB,eAAA,EAAiB,eAAe,KAAK,CAAA;AAAA,GACvC;AACF;AAoBO,MAAM,2BAA2B,CAAC;AAAA,EACvC,WAAA,GAAc;AAChB,CAAA,GAAmC,EAAC,KAAM;AACxC,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAS,CAAC,CAAA;AAEpC,EAAA,MAAM,YAAA,GAAe,CACnB,MAAA,EACA,QAAA,KACG;AACH,IAAA,QAAA,CAAS,QAAQ,CAAA;AAAA,EACnB,CAAA;AAEA,EAAA,4BACG,GAAA,EAAA,EACC,QAAA,EAAA;AAAA,oBAAA,IAAA;AAAA,MAAC,IAAA;AAAA,MAAA;AAAA,QACC,KAAA;AAAA,QACA,QAAA,EAAU,YAAA;AAAA,QACV,YAAA,EAAW,oBAAA;AAAA,QAEX,QAAA,EAAA;AAAA,0BAAA,GAAA,CAAC,OAAI,KAAA,EAAM,gBAAA,EAAkB,GAAG,SAAA,CAAU,CAAC,CAAA,EAAG,CAAA;AAAA,8BAC7C,GAAA,EAAA,EAAI,KAAA,EAAM,kBAAkB,GAAG,SAAA,CAAU,CAAC,CAAA,EAAG;AAAA;AAAA;AAAA,KAChD;AAAA,oBACA,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAc,KAAA,EAAO,CAAA,EAC7B,QAAA,kBAAA,GAAA;AAAA,MAAC,yBAAA;AAAA,MAAA;AAAA,QACC,QAAA,EAAS,QAAA;AAAA,QACT;AAAA;AAAA,KACF,EACF,CAAA;AAAA,oBACA,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAc,KAAA,EAAO,CAAA,EAC7B,QAAA,kBAAA,GAAA;AAAA,MAAC,yBAAA;AAAA,MAAA;AAAA,QACC,QAAA,EAAS,UAAA;AAAA,QACT;AAAA;AAAA,KACF,EACF;AAAA,GAAA,EACF,CAAA;AAEJ;;;;"}
@@ -0,0 +1,248 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { useState, useEffect } from 'react';
3
+ import { Typography, Box, Avatar, Tooltip } from '@material-ui/core';
4
+ import { makeStyles } from '@material-ui/core/styles';
5
+ import { InfoCard, Table, Link } from '@backstage/core-components';
6
+ import { EntityPeekAheadPopover } from '@backstage/plugin-catalog-react';
7
+ import CheckCircleIcon from '@material-ui/icons/CheckCircle';
8
+ import ErrorIcon from '@material-ui/icons/Error';
9
+ import HourglassEmptyIcon from '@material-ui/icons/HourglassEmpty';
10
+ import StopIcon from '@material-ui/icons/Stop';
11
+ import { useApi } from '@backstage/core-plugin-api';
12
+ import { bitbucketApiRef } from '../../api/BitbucketApi.esm.js';
13
+
14
+ const useStyles = makeStyles((theme) => ({
15
+ avatar: {
16
+ width: theme.spacing(2.5),
17
+ height: theme.spacing(2.5),
18
+ fontSize: "0.8rem"
19
+ },
20
+ authorContainer: {
21
+ display: "flex",
22
+ alignItems: "center",
23
+ gap: theme.spacing(1)
24
+ },
25
+ reviewersContainer: {
26
+ display: "flex",
27
+ flexWrap: "nowrap",
28
+ marginTop: theme.spacing(0.5),
29
+ marginLeft: theme.spacing(3)
30
+ },
31
+ reviewerAvatar: {
32
+ width: theme.spacing(2.5),
33
+ height: theme.spacing(2.5),
34
+ fontSize: "0.8rem",
35
+ marginLeft: theme.spacing(-0.7),
36
+ border: "1px solid white",
37
+ "&:first-child": {
38
+ marginLeft: 0
39
+ }
40
+ },
41
+ authorSection: {
42
+ display: "flex",
43
+ flexDirection: "column",
44
+ gap: theme.spacing(1)
45
+ },
46
+ moreReviewersText: {
47
+ alignSelf: "center",
48
+ cursor: "pointer",
49
+ paddingLeft: theme.spacing(0.6),
50
+ "&:hover": {
51
+ textDecoration: "underline"
52
+ }
53
+ },
54
+ buildIcon: {
55
+ fontSize: 20
56
+ }
57
+ }));
58
+ const BuildIcon = ({ state }) => {
59
+ const classes = useStyles();
60
+ const getIcon = () => {
61
+ switch (state) {
62
+ case "SUCCESSFUL":
63
+ return /* @__PURE__ */ jsx(CheckCircleIcon, { color: "primary", className: classes.buildIcon });
64
+ case "FAILED":
65
+ return /* @__PURE__ */ jsx(ErrorIcon, { color: "error", className: classes.buildIcon });
66
+ case "INPROGRESS":
67
+ return /* @__PURE__ */ jsx(HourglassEmptyIcon, { color: "action", className: classes.buildIcon });
68
+ case "STOPPED":
69
+ return /* @__PURE__ */ jsx(StopIcon, { color: "error", className: classes.buildIcon });
70
+ default:
71
+ return null;
72
+ }
73
+ };
74
+ const statusText = state ? state.charAt(0) + state.slice(1).toLowerCase() : "Unknown";
75
+ return /* @__PURE__ */ jsx(Tooltip, { title: statusText, arrow: true, children: /* @__PURE__ */ jsx(Box, { display: "flex", alignItems: "center", children: getIcon() }) });
76
+ };
77
+ const HomePagePullRequestsTable = ({
78
+ userRole,
79
+ maxItems = 25,
80
+ buildStatus = true
81
+ }) => {
82
+ const classes = useStyles();
83
+ const [pullRequests, setPullRequests] = useState([]);
84
+ const bitbucketApi = useApi(bitbucketApiRef);
85
+ const [loading, setLoading] = useState(true);
86
+ const [error, setError] = useState(null);
87
+ useEffect(() => {
88
+ const loadPullRequests = async () => {
89
+ try {
90
+ setLoading(true);
91
+ const response = await bitbucketApi.fetchUserPullRequests(
92
+ userRole,
93
+ "OPEN",
94
+ maxItems,
95
+ { includeBuildStatus: buildStatus }
96
+ );
97
+ setPullRequests(response.slice(0, maxItems));
98
+ } catch (err) {
99
+ const errorMessage = err instanceof Error ? err.message : String(err);
100
+ setError(`Failed to load pull requests: ${errorMessage}`);
101
+ } finally {
102
+ setLoading(false);
103
+ }
104
+ };
105
+ loadPullRequests();
106
+ }, [maxItems, bitbucketApi, userRole, buildStatus]);
107
+ const baseColumns = [
108
+ {
109
+ title: "PR ID",
110
+ field: "id",
111
+ width: "8%",
112
+ render: (row) => /* @__PURE__ */ jsx(Box, { display: "flex", alignItems: "center", children: /* @__PURE__ */ jsxs(Link, { to: row.url, target: "_blank", children: [
113
+ "PR #",
114
+ row.id
115
+ ] }) })
116
+ },
117
+ {
118
+ title: "Title",
119
+ field: "title",
120
+ render: (row) => /* @__PURE__ */ jsx(Typography, { variant: "body2", children: row.title })
121
+ },
122
+ {
123
+ title: "Repository",
124
+ field: "repository",
125
+ render: (row) => /* @__PURE__ */ jsx(Typography, { variant: "body2", children: /* @__PURE__ */ jsx(Link, { to: `${row.repoUrl}?at=${row.sourceBranch}`, target: "_blank", children: row.fromRepo }) })
126
+ },
127
+ {
128
+ title: "Branch",
129
+ field: "branch",
130
+ render: (row) => /* @__PURE__ */ jsx(Typography, { variant: "body2", children: /* @__PURE__ */ jsx(Link, { to: `${row.repoUrl}?at=${row.sourceBranch}`, target: "_blank", children: row.sourceBranch }) })
131
+ },
132
+ {
133
+ title: "Author/Reviewers",
134
+ field: "author.displayName",
135
+ render: (row) => {
136
+ const userEntityRef = `user:default/${row.author.slug}`;
137
+ const userAvatarUrl = `https://bitbucket.athenahealth.com/users/${row.author.slug}/avatar.png`;
138
+ return /* @__PURE__ */ jsxs("div", { className: classes.authorSection, children: [
139
+ /* @__PURE__ */ jsx("div", { className: classes.authorContainer, children: /* @__PURE__ */ jsx(EntityPeekAheadPopover, { entityRef: userEntityRef, children: /* @__PURE__ */ jsxs("div", { className: classes.authorContainer, children: [
140
+ /* @__PURE__ */ jsx(
141
+ Avatar,
142
+ {
143
+ src: userAvatarUrl,
144
+ alt: row.author.displayName,
145
+ className: classes.avatar,
146
+ children: row.author.displayName.charAt(0).toUpperCase()
147
+ }
148
+ ),
149
+ /* @__PURE__ */ jsx(Typography, { variant: "body2", children: row.author.displayName })
150
+ ] }) }) }),
151
+ row.reviewers.length > 0 && /* @__PURE__ */ jsxs("div", { className: classes.reviewersContainer, children: [
152
+ row.reviewers.slice(0, 4).map((reviewer) => /* @__PURE__ */ jsx(
153
+ Tooltip,
154
+ {
155
+ title: reviewer.displayName,
156
+ arrow: true,
157
+ children: /* @__PURE__ */ jsx(
158
+ Avatar,
159
+ {
160
+ src: `https://bitbucket.athenahealth.com/users/${reviewer.slug}/avatar.png`,
161
+ alt: reviewer.displayName,
162
+ className: classes.reviewerAvatar,
163
+ children: reviewer.displayName.charAt(0).toUpperCase()
164
+ }
165
+ )
166
+ },
167
+ reviewer.slug
168
+ )),
169
+ row.reviewers.length > 4 && /* @__PURE__ */ jsx(
170
+ Tooltip,
171
+ {
172
+ title: /* @__PURE__ */ jsx("div", { children: row.reviewers.slice(4).map((reviewer) => /* @__PURE__ */ jsxs("div", { style: { margin: "4px 0" }, children: [
173
+ /* @__PURE__ */ jsx(
174
+ Avatar,
175
+ {
176
+ src: `https://bitbucket.athenahealth.com/users/${reviewer.slug}/avatar.png`,
177
+ style: {
178
+ width: 20,
179
+ height: 20,
180
+ display: "inline-block",
181
+ marginRight: 8,
182
+ verticalAlign: "middle"
183
+ },
184
+ children: reviewer.displayName.charAt(0).toUpperCase()
185
+ }
186
+ ),
187
+ reviewer.displayName
188
+ ] }, reviewer.slug)) }),
189
+ arrow: true,
190
+ placement: "top",
191
+ children: /* @__PURE__ */ jsxs(
192
+ Typography,
193
+ {
194
+ variant: "caption",
195
+ className: classes.moreReviewersText,
196
+ children: [
197
+ "+",
198
+ row.reviewers.length - 4,
199
+ " more"
200
+ ]
201
+ }
202
+ )
203
+ }
204
+ )
205
+ ] })
206
+ ] });
207
+ }
208
+ }
209
+ ];
210
+ const buildStatusColumn = {
211
+ title: "Build Status",
212
+ field: "buildSummaries",
213
+ width: "5%",
214
+ headerStyle: {
215
+ textAlign: "center",
216
+ padding: "0 8px"
217
+ },
218
+ cellStyle: {
219
+ display: "flex",
220
+ justifyContent: "center",
221
+ alignItems: "center",
222
+ padding: "0 8px",
223
+ height: "100%",
224
+ minHeight: "48px"
225
+ // Ensure consistent row height
226
+ },
227
+ render: (row) => /* @__PURE__ */ jsx(Box, { display: "flex", justifyContent: "center", children: /* @__PURE__ */ jsx(BuildIcon, { state: row.buildStatus }) })
228
+ };
229
+ const columns = buildStatus ? [...baseColumns, buildStatusColumn] : baseColumns;
230
+ if (error) {
231
+ return /* @__PURE__ */ jsx(InfoCard, { children: /* @__PURE__ */ jsx(Typography, { color: "error", children: error }) });
232
+ }
233
+ return /* @__PURE__ */ jsx(InfoCard, { noPadding: true, children: /* @__PURE__ */ jsx(
234
+ Table,
235
+ {
236
+ options: {
237
+ padding: "dense"
238
+ },
239
+ title: `Pull Requests (${pullRequests.length})`,
240
+ data: pullRequests,
241
+ columns,
242
+ isLoading: loading
243
+ }
244
+ ) });
245
+ };
246
+
247
+ export { HomePagePullRequestsTable };
248
+ //# sourceMappingURL=HomePagePullRequestsTable.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HomePagePullRequestsTable.esm.js","sources":["../../../src/components/HomePage/HomePagePullRequestsTable.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { useState, useEffect } from 'react';\nimport { Typography, Box, Avatar, Tooltip } from '@material-ui/core';\nimport { makeStyles } from '@material-ui/core/styles';\nimport { Table, TableColumn, InfoCard, Link } from '@backstage/core-components';\nimport { EntityPeekAheadPopover } from '@backstage/plugin-catalog-react';\nimport CheckCircleIcon from '@material-ui/icons/CheckCircle';\nimport ErrorIcon from '@material-ui/icons/Error';\nimport HourglassEmptyIcon from '@material-ui/icons/HourglassEmpty';\nimport StopIcon from '@material-ui/icons/Stop';\nimport { useApi } from '@backstage/core-plugin-api';\nimport { bitbucketApiRef, PullRequest } from '../../api/BitbucketApi';\n\nconst useStyles = makeStyles(theme => ({\n avatar: {\n width: theme.spacing(2.5),\n height: theme.spacing(2.5),\n fontSize: '0.8rem',\n },\n authorContainer: {\n display: 'flex',\n alignItems: 'center',\n gap: theme.spacing(1),\n },\n reviewersContainer: {\n display: 'flex',\n flexWrap: 'nowrap',\n marginTop: theme.spacing(0.5),\n marginLeft: theme.spacing(3),\n },\n reviewerAvatar: {\n width: theme.spacing(2.5),\n height: theme.spacing(2.5),\n fontSize: '0.8rem',\n marginLeft: theme.spacing(-0.7),\n border: '1px solid white',\n '&:first-child': {\n marginLeft: 0,\n },\n },\n authorSection: {\n display: 'flex',\n flexDirection: 'column',\n gap: theme.spacing(1),\n },\n moreReviewersText: {\n alignSelf: 'center',\n cursor: 'pointer',\n paddingLeft: theme.spacing(0.6),\n '&:hover': {\n textDecoration: 'underline',\n },\n },\n buildIcon: {\n fontSize: 20,\n },\n}));\n\n// This is a React component (capital first letter) so it can use hooks\nconst BuildIcon = ({ state }: { state: string | undefined }) => {\n const classes = useStyles();\n\n const getIcon = () => {\n switch (state) {\n case 'SUCCESSFUL':\n return (\n <CheckCircleIcon color=\"primary\" className={classes.buildIcon} />\n );\n case 'FAILED':\n return <ErrorIcon color=\"error\" className={classes.buildIcon} />;\n case 'INPROGRESS':\n return (\n <HourglassEmptyIcon color=\"action\" className={classes.buildIcon} />\n );\n case 'STOPPED':\n return <StopIcon color=\"error\" className={classes.buildIcon} />;\n default:\n return null;\n }\n };\n\n const statusText = state\n ? state.charAt(0) + state.slice(1).toLowerCase()\n : 'Unknown';\n\n return (\n <Tooltip title={statusText} arrow>\n <Box display=\"flex\" alignItems=\"center\">\n {getIcon()}\n </Box>\n </Tooltip>\n );\n};\n\nexport interface BitbucketPullRequestsProps {\n maxItems?: number;\n userRole?: 'REVIEWER' | 'AUTHOR';\n buildStatus?: boolean;\n}\n\nexport const HomePagePullRequestsTable = ({\n userRole,\n maxItems = 25,\n buildStatus = true,\n}: BitbucketPullRequestsProps) => {\n const classes = useStyles();\n const [pullRequests, setPullRequests] = useState<PullRequest[]>([]);\n const bitbucketApi = useApi(bitbucketApiRef);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const loadPullRequests = async () => {\n try {\n setLoading(true);\n const response = await bitbucketApi.fetchUserPullRequests(\n userRole,\n 'OPEN',\n maxItems,\n { includeBuildStatus: buildStatus },\n );\n setPullRequests(response.slice(0, maxItems));\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err);\n setError(`Failed to load pull requests: ${errorMessage}`);\n } finally {\n setLoading(false);\n }\n };\n\n loadPullRequests();\n }, [maxItems, bitbucketApi, userRole, buildStatus]);\n\n const baseColumns: TableColumn<PullRequest>[] = [\n {\n title: 'PR ID',\n field: 'id',\n width: '8%',\n render: row => (\n <Box display=\"flex\" alignItems=\"center\">\n <Link to={row.url} target=\"_blank\">\n PR #{row.id}\n </Link>\n </Box>\n ),\n },\n {\n title: 'Title',\n field: 'title',\n render: row => <Typography variant=\"body2\">{row.title}</Typography>,\n },\n {\n title: 'Repository',\n field: 'repository',\n render: row => (\n <Typography variant=\"body2\">\n <Link to={`${row.repoUrl}?at=${row.sourceBranch}`} target=\"_blank\">\n {row.fromRepo}\n </Link>\n </Typography>\n ),\n },\n {\n title: 'Branch',\n field: 'branch',\n render: row => (\n <Typography variant=\"body2\">\n <Link to={`${row.repoUrl}?at=${row.sourceBranch}`} target=\"_blank\">\n {row.sourceBranch}\n </Link>\n </Typography>\n ),\n },\n {\n title: 'Author/Reviewers',\n field: 'author.displayName',\n render: row => {\n const userEntityRef = `user:default/${row.author.slug}`;\n const userAvatarUrl = `https://bitbucket.athenahealth.com/users/${row.author.slug}/avatar.png`;\n\n return (\n <div className={classes.authorSection}>\n <div className={classes.authorContainer}>\n <EntityPeekAheadPopover entityRef={userEntityRef}>\n <div className={classes.authorContainer}>\n <Avatar\n src={userAvatarUrl}\n alt={row.author.displayName}\n className={classes.avatar}\n >\n {row.author.displayName.charAt(0).toUpperCase()}\n </Avatar>\n <Typography variant=\"body2\">\n {row.author.displayName}\n </Typography>\n </div>\n </EntityPeekAheadPopover>\n </div>\n {row.reviewers.length > 0 && (\n <div className={classes.reviewersContainer}>\n {row.reviewers.slice(0, 4).map((reviewer: any) => (\n <Tooltip\n key={reviewer.slug}\n title={reviewer.displayName}\n arrow\n >\n <Avatar\n src={`https://bitbucket.athenahealth.com/users/${reviewer.slug}/avatar.png`}\n alt={reviewer.displayName}\n className={classes.reviewerAvatar}\n >\n {reviewer.displayName.charAt(0).toUpperCase()}\n </Avatar>\n </Tooltip>\n ))}\n {row.reviewers.length > 4 && (\n <Tooltip\n title={\n <div>\n {row.reviewers.slice(4).map((reviewer: any) => (\n <div key={reviewer.slug} style={{ margin: '4px 0' }}>\n <Avatar\n src={`https://bitbucket.athenahealth.com/users/${reviewer.slug}/avatar.png`}\n style={{\n width: 20,\n height: 20,\n display: 'inline-block',\n marginRight: 8,\n verticalAlign: 'middle',\n }}\n >\n {reviewer.displayName.charAt(0).toUpperCase()}\n </Avatar>\n {reviewer.displayName}\n </div>\n ))}\n </div>\n }\n arrow\n placement=\"top\"\n >\n <Typography\n variant=\"caption\"\n className={classes.moreReviewersText}\n >\n +{row.reviewers.length - 4} more\n </Typography>\n </Tooltip>\n )}\n </div>\n )}\n </div>\n );\n },\n },\n ];\n\n const buildStatusColumn: TableColumn<PullRequest> = {\n title: 'Build Status',\n field: 'buildSummaries',\n width: '5%',\n headerStyle: {\n textAlign: 'center',\n padding: '0 8px',\n },\n cellStyle: {\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'center',\n padding: '0 8px',\n height: '100%',\n minHeight: '48px', // Ensure consistent row height\n },\n render: row => (\n <Box display=\"flex\" justifyContent=\"center\">\n <BuildIcon state={row.buildStatus} />\n </Box>\n ),\n };\n\n const columns = buildStatus\n ? [...baseColumns, buildStatusColumn]\n : baseColumns;\n\n if (error) {\n return (\n <InfoCard>\n <Typography color=\"error\">{error}</Typography>\n </InfoCard>\n );\n }\n\n return (\n <InfoCard noPadding>\n <Table\n options={{\n padding: 'dense',\n }}\n title={`Pull Requests (${pullRequests.length})`}\n data={pullRequests}\n columns={columns}\n isLoading={loading}\n />\n </InfoCard>\n );\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { useState, useEffect } from 'react';
3
+ import { Table, MarkdownContent, Link } from '@backstage/core-components';
4
+ import { DateTime } from 'luxon';
5
+ import { useApi } from '@backstage/core-plugin-api';
6
+ import { Box, Typography } from '@material-ui/core';
7
+ import { isBitbucketSlugSet } from '../utils/isBitbucketSlugSet.esm.js';
8
+ import { bitbucketApiRef } from '../api/BitbucketApi.esm.js';
9
+ import CheckCircleIcon from '@material-ui/icons/CheckCircle';
10
+ import CancelIcon from '@material-ui/icons/Cancel';
11
+ import HourglassEmptyIcon from '@material-ui/icons/HourglassEmpty';
12
+ import Tooltip from '@material-ui/core/Tooltip';
13
+ import StatusFilter from './StatusFilter.esm.js';
14
+ import { useEntity } from '@backstage/plugin-catalog-react';
15
+
16
+ const GetElapsedTime = ({ start }) => DateTime.fromMillis(start).toRelative();
17
+ const RenderStateIcon = ({ status }) => {
18
+ switch (status) {
19
+ case "OPEN":
20
+ return /* @__PURE__ */ jsx(Tooltip, { title: "Open", placement: "top", children: /* @__PURE__ */ jsx(Typography, { component: "span", children: /* @__PURE__ */ jsx(HourglassEmptyIcon, { color: "primary" }) }) });
21
+ case "MERGED":
22
+ return /* @__PURE__ */ jsx(Tooltip, { title: "Merged", placement: "top", children: /* @__PURE__ */ jsx(Typography, { component: "span", children: /* @__PURE__ */ jsx(CheckCircleIcon, { color: "action" }) }) });
23
+ case "DECLINED":
24
+ return /* @__PURE__ */ jsx(Tooltip, { title: "Declined", placement: "top", children: /* @__PURE__ */ jsx(Typography, { component: "span", children: /* @__PURE__ */ jsx(CancelIcon, { color: "error" }) }) });
25
+ default:
26
+ return null;
27
+ }
28
+ };
29
+ const PullRequestDetailPanel = ({ rowData }) => /* @__PURE__ */ jsx(Box, { marginLeft: "14px", children: /* @__PURE__ */ jsx(
30
+ MarkdownContent,
31
+ {
32
+ content: rowData.description ?? "_No description provided._",
33
+ dialect: "gfm"
34
+ }
35
+ ) });
36
+ const PullRequestList = () => {
37
+ const [pullRequests, setPullRequests] = useState([]);
38
+ const [stateFilter, setStateFilter] = useState("All");
39
+ const [loading, setLoading] = useState(true);
40
+ const { entity } = useEntity();
41
+ const project = isBitbucketSlugSet(entity);
42
+ const bitbucketApi = useApi(bitbucketApiRef);
43
+ const projectName = project.split("/")[0];
44
+ const repoName = project.split("/")[1];
45
+ useEffect(() => {
46
+ setLoading(true);
47
+ bitbucketApi.fetchPullRequestListForRepo(
48
+ projectName,
49
+ repoName,
50
+ stateFilter !== "All" ? stateFilter : void 0
51
+ ).then((data) => {
52
+ setPullRequests(data);
53
+ setLoading(false);
54
+ }).catch((error) => error);
55
+ }, [stateFilter, projectName, repoName, bitbucketApi]);
56
+ const columns = [
57
+ {
58
+ title: "ID",
59
+ field: "id",
60
+ highlight: true,
61
+ width: "20%",
62
+ render: (row) => /* @__PURE__ */ jsx(Box, { fontWeight: "fontWeightBold", children: /* @__PURE__ */ jsxs(Link, { to: `${row.url}`, children: [
63
+ "#",
64
+ row.id
65
+ ] }) })
66
+ },
67
+ {
68
+ title: "TITLE",
69
+ field: "title",
70
+ highlight: true,
71
+ width: "30%",
72
+ render: (rowData) => /* @__PURE__ */ jsx(Box, { fontWeight: "fontWeightBold", children: rowData.title })
73
+ },
74
+ {
75
+ title: "STATE",
76
+ field: "state",
77
+ highlight: true,
78
+ width: "10%",
79
+ render: (rowData) => /* @__PURE__ */ jsx(RenderStateIcon, { status: rowData.state })
80
+ },
81
+ {
82
+ title: "AUTHOR",
83
+ field: "author",
84
+ highlight: true,
85
+ width: "20%",
86
+ render: (row) => /* @__PURE__ */ jsx(Box, { fontWeight: "fontWeightBold", children: row.author?.displayName })
87
+ },
88
+ {
89
+ title: "CREATED",
90
+ field: "createdDate",
91
+ highlight: true,
92
+ width: "20%",
93
+ render: (row) => /* @__PURE__ */ jsx(GetElapsedTime, { start: row.createdDate })
94
+ },
95
+ {
96
+ title: "LAST UPDATED",
97
+ field: "updatedDate",
98
+ highlight: true,
99
+ width: "20%",
100
+ render: (rowData) => /* @__PURE__ */ jsx(GetElapsedTime, { start: rowData.updatedDate })
101
+ }
102
+ ];
103
+ return /* @__PURE__ */ jsx(
104
+ Table,
105
+ {
106
+ columns,
107
+ data: pullRequests,
108
+ detailPanel: PullRequestDetailPanel,
109
+ isLoading: loading,
110
+ title: /* @__PURE__ */ jsxs(Box, { display: "flex", alignItems: "center", children: [
111
+ /* @__PURE__ */ jsx(Box, { mr: 1 }),
112
+ "Bitbucket Pull Requests",
113
+ /* @__PURE__ */ jsx(Box, { position: "absolute", right: 320, top: 20, children: /* @__PURE__ */ jsx(StatusFilter, { onFilterChange: setStateFilter }) })
114
+ ] })
115
+ }
116
+ );
117
+ };
118
+
119
+ export { PullRequestList as default };
120
+ //# sourceMappingURL=PullRequestList.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PullRequestList.esm.js","sources":["../../src/components/PullRequestList.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FC, useEffect, useState } from 'react';\nimport {\n TableColumn,\n Table,\n MarkdownContent,\n Link,\n} from '@backstage/core-components';\nimport { DateTime } from 'luxon';\nimport { useApi } from '@backstage/core-plugin-api';\nimport { Box, Typography } from '@material-ui/core';\nimport { isBitbucketSlugSet } from '../utils/isBitbucketSlugSet';\n\nimport { bitbucketApiRef, PullRequest } from '../api/BitbucketApi';\nimport CheckCircleIcon from '@material-ui/icons/CheckCircle';\nimport CancelIcon from '@material-ui/icons/Cancel';\nimport HourglassEmptyIcon from '@material-ui/icons/HourglassEmpty';\nimport Tooltip from '@material-ui/core/Tooltip';\nimport StatusFilter from '../components/StatusFilter';\nimport { useEntity } from '@backstage/plugin-catalog-react';\n\nconst GetElapsedTime = ({ start }: { start: number }) =>\n DateTime.fromMillis(start).toRelative();\n\nconst RenderStateIcon = ({ status }: { status: string }) => {\n switch (status) {\n case 'OPEN':\n return (\n <Tooltip title=\"Open\" placement=\"top\">\n <Typography component=\"span\">\n <HourglassEmptyIcon color=\"primary\" />\n </Typography>\n </Tooltip>\n );\n case 'MERGED':\n return (\n <Tooltip title=\"Merged\" placement=\"top\">\n <Typography component=\"span\">\n <CheckCircleIcon color=\"action\" />\n </Typography>\n </Tooltip>\n );\n case 'DECLINED':\n return (\n <Tooltip title=\"Declined\" placement=\"top\">\n <Typography component=\"span\">\n <CancelIcon color=\"error\" />\n </Typography>\n </Tooltip>\n );\n default:\n return null;\n }\n};\n\nconst PullRequestDetailPanel = ({ rowData }: { rowData: PullRequest }) => (\n <Box marginLeft=\"14px\">\n <MarkdownContent\n content={rowData.description ?? '_No description provided._'}\n dialect=\"gfm\"\n />\n </Box>\n);\n\nconst PullRequestList: FC = () => {\n const [pullRequests, setPullRequests] = useState<PullRequest[]>([]);\n const [stateFilter, setStateFilter] = useState<string>('All');\n const [loading, setLoading] = useState(true);\n const { entity } = useEntity();\n const project = isBitbucketSlugSet(entity);\n const bitbucketApi = useApi(bitbucketApiRef);\n const projectName = project.split('/')[0];\n const repoName = project.split('/')[1];\n\n useEffect(() => {\n setLoading(true);\n bitbucketApi\n .fetchPullRequestListForRepo(\n projectName,\n repoName,\n stateFilter !== 'All' ? stateFilter : undefined,\n )\n .then(data => {\n setPullRequests(data);\n setLoading(false);\n })\n .catch(error => error);\n }, [stateFilter, projectName, repoName, bitbucketApi]);\n\n const columns: TableColumn<PullRequest>[] = [\n {\n title: 'ID',\n field: 'id',\n highlight: true,\n width: '20%',\n render: (row: Partial<PullRequest>) => (\n <Box fontWeight=\"fontWeightBold\">\n <Link to={`${row.url}`}>#{row.id}</Link>\n </Box>\n ),\n },\n {\n title: 'TITLE',\n field: 'title',\n highlight: true,\n width: '30%',\n render: rowData => <Box fontWeight=\"fontWeightBold\">{rowData.title}</Box>,\n },\n {\n title: 'STATE',\n field: 'state',\n highlight: true,\n width: '10%',\n render: rowData => <RenderStateIcon status={rowData.state} />,\n },\n {\n title: 'AUTHOR',\n field: 'author',\n highlight: true,\n width: '20%',\n render: (row: Partial<PullRequest>) => (\n <Box fontWeight=\"fontWeightBold\">{row.author?.displayName}</Box>\n ),\n },\n {\n title: 'CREATED',\n field: 'createdDate',\n highlight: true,\n width: '20%',\n render: (row: Partial<PullRequest>) => (\n <GetElapsedTime start={row.createdDate!} />\n ),\n },\n {\n title: 'LAST UPDATED',\n field: 'updatedDate',\n highlight: true,\n width: '20%',\n render: rowData => <GetElapsedTime start={rowData.updatedDate} />,\n },\n ];\n\n return (\n <Table\n columns={columns}\n data={pullRequests}\n detailPanel={PullRequestDetailPanel}\n isLoading={loading}\n title={\n <Box display=\"flex\" alignItems=\"center\">\n <Box mr={1} />\n Bitbucket Pull Requests\n <Box position=\"absolute\" right={320} top={20}>\n <StatusFilter onFilterChange={setStateFilter} />\n </Box>\n </Box>\n }\n />\n );\n};\nexport default PullRequestList;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { Routes, Route } from 'react-router';
3
+ import PullRequestList from './PullRequestList.esm.js';
4
+ import { BITBUCKET_PULL_REQUESTS_ANNOTATION } from '../utils/isBitbucketSlugSet.esm.js';
5
+ import { useEntity, MissingAnnotationEmptyState } from '@backstage/plugin-catalog-react';
6
+
7
+ const isBitbucketPullRequestsAvailable = (entity) => Boolean(entity.metadata.annotations?.[BITBUCKET_PULL_REQUESTS_ANNOTATION]);
8
+ const Router = () => {
9
+ const { entity } = useEntity();
10
+ return !isBitbucketPullRequestsAvailable(entity) ? /* @__PURE__ */ jsx(
11
+ MissingAnnotationEmptyState,
12
+ {
13
+ annotation: BITBUCKET_PULL_REQUESTS_ANNOTATION
14
+ }
15
+ ) : /* @__PURE__ */ jsx(Routes, { children: /* @__PURE__ */ jsx(Route, { path: "/", element: /* @__PURE__ */ jsx(PullRequestList, {}) }) });
16
+ };
17
+
18
+ export { Router, isBitbucketPullRequestsAvailable };
19
+ //# sourceMappingURL=Router.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Router.esm.js","sources":["../../src/components/Router.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Entity } from '@backstage/catalog-model';\nimport { Route, Routes } from 'react-router';\nimport PullRequestList from './PullRequestList';\nimport { BITBUCKET_PULL_REQUESTS_ANNOTATION } from '../utils/isBitbucketSlugSet';\nimport {\n useEntity,\n MissingAnnotationEmptyState,\n} from '@backstage/plugin-catalog-react';\n\nexport const isBitbucketPullRequestsAvailable = (entity: Entity) =>\n Boolean(entity.metadata.annotations?.[BITBUCKET_PULL_REQUESTS_ANNOTATION]);\n\nexport const Router = () => {\n const { entity } = useEntity();\n return !isBitbucketPullRequestsAvailable(entity) ? (\n <MissingAnnotationEmptyState\n annotation={BITBUCKET_PULL_REQUESTS_ANNOTATION}\n />\n ) : (\n <Routes>\n <Route path=\"/\" element={<PullRequestList />} />\n </Routes>\n );\n};\n"],"names":[],"mappings":";;;;;;AAyBO,MAAM,gCAAA,GAAmC,CAAC,MAAA,KAC/C,OAAA,CAAQ,OAAO,QAAA,CAAS,WAAA,GAAc,kCAAkC,CAAC;AAEpE,MAAM,SAAS,MAAM;AAC1B,EAAA,MAAM,EAAE,MAAA,EAAO,GAAI,SAAA,EAAU;AAC7B,EAAA,OAAO,CAAC,gCAAA,CAAiC,MAAM,CAAA,mBAC7C,GAAA;AAAA,IAAC,2BAAA;AAAA,IAAA;AAAA,MACC,UAAA,EAAY;AAAA;AAAA,GACd,mBAEA,GAAA,CAAC,MAAA,EAAA,EACC,QAAA,kBAAA,GAAA,CAAC,KAAA,EAAA,EAAM,IAAA,EAAK,GAAA,EAAI,OAAA,kBAAS,GAAA,CAAC,eAAA,EAAA,EAAgB,CAAA,EAAI,CAAA,EAChD,CAAA;AAEJ;;;;"}
@@ -0,0 +1,29 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useState } from 'react';
3
+ import { ButtonGroup, Button } from '@material-ui/core';
4
+
5
+ const StatusFilter = ({ onFilterChange }) => {
6
+ const [status, setStatus] = useState("ALL");
7
+ const handleStatusChange = (newStatus) => {
8
+ setStatus(newStatus);
9
+ onFilterChange(newStatus);
10
+ };
11
+ const buttons = [
12
+ { value: "OPEN", label: "Open" },
13
+ { value: "MERGED", label: "Merged" },
14
+ { value: "DECLINED", label: "Declined" },
15
+ { value: "ALL", label: "All" }
16
+ ];
17
+ return /* @__PURE__ */ jsx(ButtonGroup, { color: "primary", "aria-label": "pull request status filter", children: buttons.map(({ value, label }) => /* @__PURE__ */ jsx(
18
+ Button,
19
+ {
20
+ onClick: () => handleStatusChange(value),
21
+ variant: status === value ? "contained" : "outlined",
22
+ children: label
23
+ },
24
+ value
25
+ )) });
26
+ };
27
+
28
+ export { StatusFilter as default };
29
+ //# sourceMappingURL=StatusFilter.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StatusFilter.esm.js","sources":["../../src/components/StatusFilter.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FC, useState } from 'react';\nimport { ButtonGroup, Button } from '@material-ui/core';\n\ninterface StatusFilterProps {\n onFilterChange: (filter: string) => void;\n}\nconst StatusFilter: FC<StatusFilterProps> = ({ onFilterChange }) => {\n const [status, setStatus] = useState('ALL');\n\n const handleStatusChange = (newStatus: any) => {\n setStatus(newStatus);\n onFilterChange(newStatus);\n };\n\n const buttons = [\n { value: 'OPEN', label: 'Open' },\n { value: 'MERGED', label: 'Merged' },\n { value: 'DECLINED', label: 'Declined' },\n { value: 'ALL', label: 'All' },\n ];\n\n return (\n <ButtonGroup color=\"primary\" aria-label=\"pull request status filter\">\n {buttons.map(({ value, label }) => (\n <Button\n key={value}\n onClick={() => handleStatusChange(value)}\n variant={status === value ? 'contained' : 'outlined'}\n >\n {label}\n </Button>\n ))}\n </ButtonGroup>\n );\n};\n\nexport default StatusFilter;\n"],"names":[],"mappings":";;;;AAsBA,MAAM,YAAA,GAAsC,CAAC,EAAE,cAAA,EAAe,KAAM;AAClE,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,SAAS,KAAK,CAAA;AAE1C,EAAA,MAAM,kBAAA,GAAqB,CAAC,SAAA,KAAmB;AAC7C,IAAA,SAAA,CAAU,SAAS,CAAA;AACnB,IAAA,cAAA,CAAe,SAAS,CAAA;AAAA,EAC1B,CAAA;AAEA,EAAA,MAAM,OAAA,GAAU;AAAA,IACd,EAAE,KAAA,EAAO,MAAA,EAAQ,KAAA,EAAO,MAAA,EAAO;AAAA,IAC/B,EAAE,KAAA,EAAO,QAAA,EAAU,KAAA,EAAO,QAAA,EAAS;AAAA,IACnC,EAAE,KAAA,EAAO,UAAA,EAAY,KAAA,EAAO,UAAA,EAAW;AAAA,IACvC,EAAE,KAAA,EAAO,KAAA,EAAO,KAAA,EAAO,KAAA;AAAM,GAC/B;AAEA,EAAA,uBACE,GAAA,CAAC,WAAA,EAAA,EAAY,KAAA,EAAM,SAAA,EAAU,YAAA,EAAW,4BAAA,EACrC,QAAA,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAC,EAAE,KAAA,EAAO,KAAA,EAAM,qBAC3B,GAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MAEC,OAAA,EAAS,MAAM,kBAAA,CAAmB,KAAK,CAAA;AAAA,MACvC,OAAA,EAAS,MAAA,KAAW,KAAA,GAAQ,WAAA,GAAc,UAAA;AAAA,MAEzC,QAAA,EAAA;AAAA,KAAA;AAAA,IAJI;AAAA,GAMR,CAAA,EACH,CAAA;AAEJ;;;;"}
@@ -0,0 +1,37 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as _backstage_core_plugin_api from '@backstage/core-plugin-api';
3
+
4
+ /**
5
+ * Props for the HomePagePullRequestsCard component
6
+ *
7
+ * @public
8
+ */
9
+ interface HomePagePullRequestsCardProps {
10
+ /**
11
+ * Flag to determine whether to display build status information in the pull requests table.
12
+ * Defaults to true if not specified.
13
+ */
14
+ buildStatus?: boolean;
15
+ }
16
+ /**
17
+ * Component to display pull requests as tabs on the homepage
18
+ *
19
+ * @public
20
+ */
21
+ declare const HomePagePullRequestsCard: ({ buildStatus, }?: HomePagePullRequestsCardProps) => react_jsx_runtime.JSX.Element;
22
+
23
+ /**
24
+ * Plugin for Bitbucket pull requests integration
25
+ * @public
26
+ */
27
+ declare const bitbucketPlugin: _backstage_core_plugin_api.BackstagePlugin<{
28
+ root: _backstage_core_plugin_api.RouteRef<undefined>;
29
+ }, {}, {}>;
30
+ /**
31
+ * Component for displaying Bitbucket pull requests in the entity page
32
+ * @public
33
+ */
34
+ declare const EntityBitbucketPullRequestsContent: () => react_jsx_runtime.JSX.Element;
35
+
36
+ export { EntityBitbucketPullRequestsContent, HomePagePullRequestsCard, bitbucketPlugin };
37
+ export type { HomePagePullRequestsCardProps };
@@ -0,0 +1,3 @@
1
+ export { EntityBitbucketPullRequestsContent, bitbucketPlugin } from './plugin.esm.js';
2
+ export { HomePagePullRequestsCard } from './components/HomePage/HomePagePullRequestsCard.esm.js';
3
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,35 @@
1
+ import { createPlugin, createApiFactory, configApiRef, identityApiRef, discoveryApiRef, createRoutableExtension } from '@backstage/core-plugin-api';
2
+ import { rootRouteRef } from './routes.esm.js';
3
+ import { BitbucketApi, bitbucketApiRef } from './api/BitbucketApi.esm.js';
4
+ import 'react/jsx-runtime';
5
+ import 'react';
6
+ import '@material-ui/core';
7
+ import './components/HomePage/HomePagePullRequestsTable.esm.js';
8
+
9
+ const bitbucketPlugin = createPlugin({
10
+ id: "bitbucket-pullrequests",
11
+ apis: [
12
+ createApiFactory({
13
+ api: bitbucketApiRef,
14
+ deps: {
15
+ discoveryApi: discoveryApiRef,
16
+ identityApi: identityApiRef,
17
+ configApi: configApiRef
18
+ },
19
+ factory: ({ discoveryApi, identityApi, configApi }) => new BitbucketApi({ discoveryApi, identityApi, configApi })
20
+ })
21
+ ],
22
+ routes: {
23
+ root: rootRouteRef
24
+ }
25
+ });
26
+ const EntityBitbucketPullRequestsContent = bitbucketPlugin.provide(
27
+ createRoutableExtension({
28
+ name: "EntityBitbucketPullRequestsContent",
29
+ component: () => import('./components/Router.esm.js').then((m) => m.Router),
30
+ mountPoint: rootRouteRef
31
+ })
32
+ );
33
+
34
+ export { EntityBitbucketPullRequestsContent, bitbucketPlugin };
35
+ //# sourceMappingURL=plugin.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n createPlugin,\n createApiFactory,\n discoveryApiRef,\n createRoutableExtension,\n identityApiRef,\n configApiRef,\n} from '@backstage/core-plugin-api';\nimport { rootRouteRef } from './routes';\nimport { bitbucketApiRef, BitbucketApi } from './api/BitbucketApi';\n\n/**\n * Plugin for Bitbucket pull requests integration\n * @public\n */\nexport const bitbucketPlugin = createPlugin({\n id: 'bitbucket-pullrequests',\n apis: [\n createApiFactory({\n api: bitbucketApiRef,\n deps: {\n discoveryApi: discoveryApiRef,\n identityApi: identityApiRef,\n configApi: configApiRef,\n },\n factory: ({ discoveryApi, identityApi, configApi }) =>\n new BitbucketApi({ discoveryApi, identityApi, configApi }),\n }),\n ],\n routes: {\n root: rootRouteRef,\n },\n});\n\n/**\n * Component for displaying Bitbucket pull requests in the entity page\n * @public\n */\nexport const EntityBitbucketPullRequestsContent = bitbucketPlugin.provide(\n createRoutableExtension({\n name: 'EntityBitbucketPullRequestsContent',\n component: () => import('./components/Router').then(m => m.Router),\n mountPoint: rootRouteRef,\n }),\n);\n\nexport { HomePagePullRequestsCard } from './components/HomePage/HomePagePullRequestsCard';\nexport type { HomePagePullRequestsCardProps } from './components/HomePage/HomePagePullRequestsCard';\n"],"names":[],"mappings":";;;;;;;;AA+BO,MAAM,kBAAkB,YAAA,CAAa;AAAA,EAC1C,EAAA,EAAI,wBAAA;AAAA,EACJ,IAAA,EAAM;AAAA,IACJ,gBAAA,CAAiB;AAAA,MACf,GAAA,EAAK,eAAA;AAAA,MACL,IAAA,EAAM;AAAA,QACJ,YAAA,EAAc,eAAA;AAAA,QACd,WAAA,EAAa,cAAA;AAAA,QACb,SAAA,EAAW;AAAA,OACb;AAAA,MACA,OAAA,EAAS,CAAC,EAAE,YAAA,EAAc,WAAA,EAAa,SAAA,EAAU,KAC/C,IAAI,YAAA,CAAa,EAAE,YAAA,EAAc,WAAA,EAAa,WAAW;AAAA,KAC5D;AAAA,GACH;AAAA,EACA,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA;AAEV,CAAC;AAMM,MAAM,qCAAqC,eAAA,CAAgB,OAAA;AAAA,EAChE,uBAAA,CAAwB;AAAA,IACtB,IAAA,EAAM,oCAAA;AAAA,IACN,SAAA,EAAW,MAAM,OAAO,4BAAqB,EAAE,IAAA,CAAK,CAAA,CAAA,KAAK,EAAE,MAAM,CAAA;AAAA,IACjE,UAAA,EAAY;AAAA,GACb;AACH;;;;"}
@@ -0,0 +1,8 @@
1
+ import { createRouteRef } from '@backstage/core-plugin-api';
2
+
3
+ const rootRouteRef = createRouteRef({
4
+ id: "bitbucket-pullrequests"
5
+ });
6
+
7
+ export { rootRouteRef };
8
+ //# sourceMappingURL=routes.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.esm.js","sources":["../src/routes.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createRouteRef } from '@backstage/core-plugin-api';\n\nexport const rootRouteRef = createRouteRef({\n id: 'bitbucket-pullrequests',\n});\n"],"names":[],"mappings":";;AAkBO,MAAM,eAAe,cAAA,CAAe;AAAA,EACzC,EAAA,EAAI;AACN,CAAC;;;;"}
@@ -0,0 +1,7 @@
1
+ const BITBUCKET_PULL_REQUESTS_ANNOTATION = "bitbucket.com/project-slug";
2
+ const isBitbucketSlugSet = (entity) => {
3
+ return entity?.metadata.annotations?.[BITBUCKET_PULL_REQUESTS_ANNOTATION] ?? "";
4
+ };
5
+
6
+ export { BITBUCKET_PULL_REQUESTS_ANNOTATION, isBitbucketSlugSet };
7
+ //# sourceMappingURL=isBitbucketSlugSet.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isBitbucketSlugSet.esm.js","sources":["../../src/utils/isBitbucketSlugSet.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Entity } from '@backstage/catalog-model';\n\nexport const BITBUCKET_PULL_REQUESTS_ANNOTATION = 'bitbucket.com/project-slug';\n\nexport const isBitbucketSlugSet = (entity: Entity) => {\n return (\n entity?.metadata.annotations?.[BITBUCKET_PULL_REQUESTS_ANNOTATION] ?? ''\n );\n};\n"],"names":[],"mappings":"AAkBO,MAAM,kCAAA,GAAqC;AAE3C,MAAM,kBAAA,GAAqB,CAAC,MAAA,KAAmB;AACpD,EAAA,OACE,MAAA,EAAQ,QAAA,CAAS,WAAA,GAAc,kCAAkC,CAAA,IAAK,EAAA;AAE1E;;;;"}
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@backstage-community/plugin-bitbucket-pull-requests",
3
+ "version": "3.0.0",
4
+ "main": "dist/index.esm.js",
5
+ "types": "dist/index.d.ts",
6
+ "license": "Apache-2.0",
7
+ "keywords": [
8
+ "backstage",
9
+ "plugin",
10
+ "bitbucket",
11
+ "pullrequest"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "main": "dist/index.esm.js",
16
+ "types": "dist/index.d.ts"
17
+ },
18
+ "homepage": "https://backstage.io",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/backstage/community-plugins",
22
+ "directory": "workspaces/bitbucket-pull-requests/plugins/bitbucket-pull-requests"
23
+ },
24
+ "backstage": {
25
+ "role": "frontend-plugin",
26
+ "pluginId": "bitbucket-pull-requests",
27
+ "pluginPackages": [
28
+ "@backstage-community/plugin-bitbucket-pull-requests"
29
+ ]
30
+ },
31
+ "sideEffects": false,
32
+ "scripts": {
33
+ "start": "backstage-cli package start",
34
+ "build": "backstage-cli package build",
35
+ "lint": "backstage-cli package lint",
36
+ "test": "backstage-cli package test",
37
+ "clean": "backstage-cli package clean",
38
+ "prepack": "backstage-cli package prepack",
39
+ "postpack": "backstage-cli package postpack"
40
+ },
41
+ "dependencies": {
42
+ "@backstage/catalog-model": "^1.7.5",
43
+ "@backstage/core-components": "^0.17.4",
44
+ "@backstage/core-plugin-api": "^1.10.9",
45
+ "@backstage/plugin-catalog-react": "^1.19.1",
46
+ "@material-ui/core": "^4.9.13",
47
+ "@material-ui/icons": "^4.9.1",
48
+ "cross-fetch": "^4.0.0",
49
+ "luxon": "^3.0.0",
50
+ "msw": "^1.0.1",
51
+ "react-router": "^6.3.0",
52
+ "react-router-dom": "^6.3.0"
53
+ },
54
+ "peerDependencies": {
55
+ "react": "^16.13.1 || ^17.0.0 || ^18.0.0",
56
+ "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
57
+ "react-router": "6.0.0-beta.0 || ^6.3.0"
58
+ },
59
+ "devDependencies": {
60
+ "@backstage/cli": "^0.33.1",
61
+ "@backstage/core-app-api": "^1.18.0",
62
+ "@backstage/dev-utils": "^1.1.12",
63
+ "@backstage/test-utils": "^1.7.10",
64
+ "@testing-library/jest-dom": "^6.0.0",
65
+ "@testing-library/react": "^14.0.0",
66
+ "@types/luxon": "^3.0.0",
67
+ "@types/react": "^18",
68
+ "@types/react-dom": "^18",
69
+ "react": "^18",
70
+ "react-dom": "^18"
71
+ },
72
+ "files": [
73
+ "dist"
74
+ ],
75
+ "typesVersions": {
76
+ "*": {
77
+ "package.json": [
78
+ "package.json"
79
+ ]
80
+ }
81
+ },
82
+ "module": "./dist/index.esm.js"
83
+ }