From 2b32e64414e7213e83c1568bc7db1f452c908d9f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 16:33:20 +0100 Subject: [PATCH] feat(scaffolder): add GitHubRepoBranchPicker Signed-off-by: Benjamin Janssens --- .../GitHubRepoBranchPicker.test.tsx | 104 ++++++++++++++++++ .../GitHubRepoBranchPicker.tsx | 93 ++++++++++++++++ .../RepoBranchPicker/RepoBranchPicker.tsx | 15 +++ .../fields/RepoBranchPicker/types.ts | 1 + 4 files changed, 213 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx new file mode 100644 index 0000000000..afdfaf4d6e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright 2024 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 React from 'react'; +import { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; +import { GitHubRepoBranchPicker } from './GitHubRepoBranchPicker'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; + +describe('GitHubRepoBranchPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest.fn().mockResolvedValue({ results: [{ id: 'branch1' }] }), + }; + + it('renders an input field', () => { + const { getByRole } = render( + + + , + ); + + expect(getByRole('textbox')).toBeInTheDocument(); + expect(getByRole('textbox')).toHaveValue('main'); + }); + + it('calls onChange when the input field changes', () => { + const onChange = jest.fn(); + + const { getByRole } = render( + + + , + ); + + const input = getByRole('textbox'); + + act(() => { + input.focus(); + fireEvent.change(input, { + target: { value: 'develop' }, + }); + input.blur(); + }); + + expect(onChange).toHaveBeenCalledWith({ branch: 'develop' }); + }); + + it('should populate branches', async () => { + const onChange = jest.fn(); + + const { getByRole, getByText } = render( + + + , + ); + + // Open the Autcomplete dropdown + const input = getByRole('textbox'); + await userEvent.click(input); + + // Verify that the available workspaces are shown + await waitFor(() => expect(getByText('branch1')).toBeInTheDocument()); + + // Verify that selecting an option calls onChange + await userEvent.click(getByText('branch1')); + expect(onChange).toHaveBeenCalledWith({ + branch: 'branch1', + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx new file mode 100644 index 0000000000..aaf448ae6c --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2024 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 { useApi } from '@backstage/core-plugin-api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import React, { useCallback, useState } from 'react'; +import useDebounce from 'react-use/esm/useDebounce'; +import { BaseRepoBranchPickerProps } from './types'; + +/** + * The underlying component that is rendered in the form for the `GitHubRepoBranchPicker` + * field extension. + * + * @public + * + */ +export const GitHubRepoBranchPicker = ({ + onChange, + state, + rawErrors, + accessToken, + required, +}: BaseRepoBranchPickerProps<{ + accessToken?: string; +}>) => { + const { owner, repository, branch } = state; + + const [availableBranches, setAvailableBranches] = useState([]); + + const scaffolderApi = useApi(scaffolderApiRef); + + const updateAvailableBranches = useCallback(() => { + if (!scaffolderApi.autocomplete || !owner || !repository || !accessToken) { + setAvailableBranches([]); + return; + } + + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'branches', + context: { owner, repository }, + provider: 'github', + }) + .then(({ results }) => { + setAvailableBranches(results.map(r => r.id)); + }) + .catch(() => { + setAvailableBranches([]); + }); + }, [owner, repository, accessToken, scaffolderApi]); + + useDebounce(updateAvailableBranches, 500, [updateAvailableBranches]); + + return ( + 0 && !branch} + > + { + onChange({ branch: newValue || '' }); + }} + options={availableBranches} + renderInput={params => ( + + )} + freeSolo + autoSelect + /> + The branch of the repository + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx index 4ca22c8f80..98b1d73b8d 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx @@ -30,6 +30,7 @@ import { RepoBranchPickerProps } from './schema'; import { RepoBranchPickerState } from './types'; import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; import { DefaultRepoBranchPicker } from './DefaultRepoBranchPicker'; +import { GitHubRepoBranchPicker } from './GitHubRepoBranchPicker'; /** * The underlying component that is rendered in the form for the `RepoBranchPicker` @@ -102,6 +103,7 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { host: url.host, workspace: url.searchParams.get('workspace') || '', repository: url.searchParams.get('repo') || '', + owner: url.searchParams.get('owner') || '', })); } }, [repoUrl]); @@ -134,6 +136,19 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { required={required} /> ); + case 'github': + return ( + + ); default: return ( = T & {