diff --git a/packages/integration/src/bitbucketCloud/config.ts b/packages/integration/src/bitbucketCloud/config.ts index 5ecabfedc4..2f533480cc 100644 --- a/packages/integration/src/bitbucketCloud/config.ts +++ b/packages/integration/src/bitbucketCloud/config.ts @@ -46,6 +46,11 @@ export type BitbucketCloudIntegrationConfig = { * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/ */ appPassword?: string; + + /** + * The access token to use for requests to Bitbucket Cloud (bitbucket.org). + */ + accessToken?: string; }; /** diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts index 3ac005fe2b..97d3d0e52e 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts @@ -69,6 +69,22 @@ export class BitbucketCloudClient { ); } + listProjectsByWorkspace( + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination { + const workspaceEnc = encodeURIComponent(workspace); + + return new WithPagination( + paginationOptions => + this.createUrl(`/workspaces/${workspaceEnc}/projects`, { + ...paginationOptions, + ...options, + }), + url => this.getTypeMapped(url), + ); + } + private createUrl(endpoint: string, options?: RequestOptions): URL { const request = new URL(this.config.apiBaseUrl + endpoint); for (const key in options) { @@ -115,6 +131,9 @@ export class BitbucketCloudClient { headers.Authorization = `Basic ${buffer.toString('base64')}`; } + if (this.config.accessToken) + headers.Authorization = `Bearer ${this.config.accessToken}`; + return headers; } } diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index 8fce389a32..ace8ee4939 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -253,6 +253,17 @@ export namespace Models { values?: Set; } + /** + * A paginated list of projects. + * @public + */ + export interface PaginatedProjects extends Paginated { + /** + * The values of the current page. + */ + values?: Set; + } + /** * Object describing a user's role on resources like commits or pull requests. * @public diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index d1b58c8eca..a17fc14105 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -67,6 +67,7 @@ "@backstage/frontend-plugin-api": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/integration-react": "workspace:^", + "@backstage/plugin-bitbucket-cloud-common": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index d070e3fee5..5227f8a093 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; import { Select, SelectItem } from '@backstage/core-components'; import { RepoUrlPickerState } from './types'; +import { BitbucketCloudClient } from '@backstage/plugin-bitbucket-cloud-common'; /** * The underlying component that is rendered in the form for the `BitbucketRepoPicker` @@ -36,6 +37,7 @@ export const BitbucketRepoPicker = (props: { onChange: (state: RepoUrlPickerState) => void; state: RepoUrlPickerState; rawErrors: string[]; + accessToken?: string; }) => { const { allowedOwners = [], @@ -43,6 +45,7 @@ export const BitbucketRepoPicker = (props: { onChange, rawErrors, state, + accessToken, } = props; const { host, workspace, project } = state; const ownerItems: SelectItem[] = allowedOwners @@ -58,6 +61,53 @@ export const BitbucketRepoPicker = (props: { } }, [allowedOwners, host, onChange]); + const [client, setClient] = useState(); + const [availableProjects, setAvailableProjects] = useState([]); + const [availableRepositories, setAvailableRepositories] = useState( + [], + ); + + useEffect(() => { + if (accessToken) + setClient( + BitbucketCloudClient.fromConfig({ + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + accessToken, + }), + ); + }, [accessToken]); + + const onChangeWorkspace = async () => { + if (client) + if (!workspace) { + setAvailableProjects([]); + } else { + for await (const page of client + .listProjectsByWorkspace(workspace) + .iteratePages()) { + const keys = [...page.values!].map(p => p.key!); + setAvailableProjects([...availableProjects, ...keys]); + } + } + }; + + const onChangeProject = async () => { + if (client && workspace) + if (!project) { + setAvailableRepositories([]); + } else { + for await (const page of client + .listRepositoriesByWorkspace(workspace, { + q: `project.key="${project}"`, + }) + .iteratePages()) { + const keys = [...page.values!].map(p => p.slug!); + setAvailableRepositories([...availableRepositories, ...keys]); + } + } + }; + return ( <> {host === 'bitbucket.org' && ( @@ -84,6 +134,7 @@ export const BitbucketRepoPicker = (props: { id="workspaceInput" onChange={e => onChange({ workspace: e.target.value })} value={workspace} + onBlur={() => onChangeWorkspace()} /> )} @@ -115,6 +166,7 @@ export const BitbucketRepoPicker = (props: { id="projectInput" onChange={e => onChange({ project: e.target.value })} value={project} + onBlur={() => onChangeProject()} /> )} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 85245639b0..cd749a08a1 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -203,6 +203,10 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { rawErrors={rawErrors} state={state} onChange={updateLocalState} + accessToken={ + uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey && + secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey] + } /> )} {hostType === 'azure' && ( diff --git a/yarn.lock b/yarn.lock index f0f0d02a5d..1719070c2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7268,6 +7268,7 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/integration-react": "workspace:^" + "@backstage/plugin-bitbucket-cloud-common": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^"