diff --git a/.changeset/wet-tips-yawn.md b/.changeset/wet-tips-yawn.md new file mode 100644 index 0000000000..63f3c9dd2e --- /dev/null +++ b/.changeset/wet-tips-yawn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +The `RepoUrlPicker` field extension now has an `allowedProjects` option for narrowing the selection of Bitbucket URLs. diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml index ee1bc634b4..58955582d2 100644 --- a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml @@ -20,7 +20,17 @@ spec: ui:options: allowedHosts: - bitbucket.org - - server.bitbucket.com + # The rest of these options are optional. + # You can read more at: https://backstage.io/docs/reference/plugin-scaffolder.repourlpickerfieldextension + # allowedOwners: + # - WORKSPACE1 + # - WORKSPACE2 + # allowedProjects: + # - PROJECT1 + # - PROJECT2 + # allowedRepos: + # - REPO1 + # - REPO2 - title: Fill in some steps required: - name diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 39e58a7a42..a8dcdd76ed 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -340,6 +340,7 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent< { allowedOwners?: string[] | undefined; allowedOrganizations?: string[] | undefined; + allowedProjects?: string[] | undefined; allowedRepos?: string[] | undefined; allowedHosts?: string[] | undefined; requestUserCredentials?: @@ -365,6 +366,7 @@ export const RepoUrlPickerFieldSchema: FieldSchema< { allowedOwners?: string[] | undefined; allowedOrganizations?: string[] | undefined; + allowedProjects?: string[] | undefined; allowedRepos?: string[] | undefined; allowedHosts?: string[] | undefined; requestUserCredentials?: diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx index fc643e7360..f799d68c2d 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx @@ -56,6 +56,7 @@ describe('BitbucketRepoPicker', () => { expect(getAllByRole('textbox')).toHaveLength(1); }); + describe('workspace field', () => { it('calls onChange when the workspace changes', () => { const onChange = jest.fn(); @@ -92,5 +93,47 @@ describe('BitbucketRepoPicker', () => { expect(onChange).toHaveBeenCalledWith({ project: 'test-project' }); }); + + it('Does not render a select if the list of allowed projects does not exist', async () => { + const { getAllByRole } = render( + , + ); + + expect(getAllByRole('textbox')).toHaveLength(2); + expect(getAllByRole('textbox')[1]).toHaveValue(''); + }); + + it('Does not render a select if the list of allowed projects is empty', async () => { + const { getAllByRole } = render( + , + ); + + expect(getAllByRole('textbox')).toHaveLength(2); + expect(getAllByRole('textbox')[1]).toHaveValue(''); + }); + + it('Does render a select if there is a list of allowed projects', async () => { + const allowedProjects = ['project1', 'project2']; + const { findByText } = render( + , + ); + + expect(await findByText('project1')).toBeInTheDocument(); + expect(await findByText('project2')).toBeInTheDocument(); + }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 5bc1380553..d070e3fee5 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -21,17 +21,36 @@ import InputLabel from '@material-ui/core/InputLabel'; import { Select, SelectItem } from '@backstage/core-components'; import { RepoUrlPickerState } from './types'; +/** + * The underlying component that is rendered in the form for the `BitbucketRepoPicker` + * field extension. + * + * @public + * @param allowedOwners - Allowed workspaces for the Bitbucket cloud repository + * @param allowedProjects - Allowed projects for the Bitbucket cloud repository + * + */ export const BitbucketRepoPicker = (props: { allowedOwners?: string[]; + allowedProjects?: string[]; onChange: (state: RepoUrlPickerState) => void; state: RepoUrlPickerState; rawErrors: string[]; }) => { - const { allowedOwners = [], onChange, rawErrors, state } = props; + const { + allowedOwners = [], + allowedProjects = [], + onChange, + rawErrors, + state, + } = props; const { host, workspace, project } = state; const ownerItems: SelectItem[] = allowedOwners ? allowedOwners?.map(i => ({ label: i, value: i })) : []; + const projectItems: SelectItem[] = allowedProjects + ? allowedProjects?.map(i => ({ label: i, value: i })) + : []; useEffect(() => { if (host === 'bitbucket.org' && allowedOwners.length) { @@ -69,7 +88,7 @@ export const BitbucketRepoPicker = (props: { )} - The Organization that this repo will belong to + The Workspace that this repo will belong to )} @@ -78,12 +97,27 @@ export const BitbucketRepoPicker = (props: { required error={rawErrors?.length > 0 && !project} > - Project - onChange({ project: e.target.value })} - value={project} - /> + {allowedProjects?.length ? ( + onChange({ project: e.target.value })} + value={project} + /> + + )} The Project that this repo will belong to diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 827eff6ea2..79b7f23da6 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -60,12 +60,16 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { () => uiSchema?.['ui:options']?.allowedOwners ?? [], [uiSchema], ); + const allowedProjects = useMemo( + () => uiSchema?.['ui:options']?.allowedProjects ?? [], + [uiSchema], + ); const allowedRepos = useMemo( () => uiSchema?.['ui:options']?.allowedRepos ?? [], [uiSchema], ); - const { owner, organization, repoName } = state; + const { owner, organization, project, repoName } = state; useEffect(() => { onChange(serializeRepoPickerUrl(state)); @@ -90,6 +94,15 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { } }, [setState, allowedOwners, owner]); + useEffect(() => { + if (allowedProjects.length > 0 && !project) { + setState(prevState => ({ + ...prevState, + project: allowedProjects[0], + })); + } + }, [setState, allowedProjects, project]); + useEffect(() => { if (allowedRepos.length > 0 && !repoName) { setState(prevState => ({ ...prevState, repoName: allowedRepos[0] })); @@ -169,6 +182,7 @@ export const RepoUrlPicker = (props: RepoUrlPickerProps) => { {hostType === 'bitbucket' && (