From 3e5acb5e97c59ecdd2d63dd13ed64fe7c6b47ed2 Mon Sep 17 00:00:00 2001 From: asheen1234 Date: Mon, 30 Mar 2026 14:34:07 -0400 Subject: [PATCH] Add GitLabRepoOwnerPicker and tests Signed-off-by: asheen1234 --- .changeset/legal-results-kneel.md | 5 + .../GitLabRepoOwnerPicker.test.tsx | 149 ++++++++++++++++++ .../RepoOwnerPicker/GitLabRepoOwnerPicker.tsx | 113 +++++++++++++ .../RepoOwnerPicker/RepoOwnerPicker.tsx | 17 ++ 4 files changed, 284 insertions(+) create mode 100644 .changeset/legal-results-kneel.md create mode 100644 plugins/scaffolder/src/components/fields/RepoOwnerPicker/GitLabRepoOwnerPicker.test.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoOwnerPicker/GitLabRepoOwnerPicker.tsx diff --git a/.changeset/legal-results-kneel.md b/.changeset/legal-results-kneel.md new file mode 100644 index 0000000000..68b8c582cc --- /dev/null +++ b/.changeset/legal-results-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added to field extension 'RepoOwnerPicker' for retrieving GitLab repository owners. diff --git a/plugins/scaffolder/src/components/fields/RepoOwnerPicker/GitLabRepoOwnerPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoOwnerPicker/GitLabRepoOwnerPicker.test.tsx new file mode 100644 index 0000000000..ff4e5c151c --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoOwnerPicker/GitLabRepoOwnerPicker.test.tsx @@ -0,0 +1,149 @@ +/* + * Copyright 2025 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 { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; +import { GitLabRepoOwnerPicker } from './GitLabRepoOwnerPicker'; +import { act, fireEvent, waitFor, screen } from '@testing-library/react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; + +describe('GitLabRepoOwnerPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest + .fn() + .mockResolvedValue({ results: [{ id: 'owner1' }, { id: 'owner2' }] }), + }; + + it('renders an input field', async () => { + const { getByRole } = await renderInTestApp( + + + , + ); + + expect(getByRole('textbox')).toBeInTheDocument(); + expect(getByRole('textbox')).toHaveValue('owner1'); + }); + + it('input field disabled', async () => { + await renderInTestApp( + + + , + ); + + const input = screen.getByRole('textbox'); + + // Expect input to be disabled + expect(input).toBeDisabled(); + expect(input).toHaveValue('owner1'); + }); + + it('calls onChange when the input field changes', async () => { + const onChange = jest.fn(); + + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + act(() => { + input.focus(); + fireEvent.change(input, { + target: { value: 'owner2' }, + }); + input.blur(); + }); + + expect(onChange).toHaveBeenCalledWith({ owner: 'owner2' }); + }); + + it('should populate owners', async () => { + const onChange = jest.fn(); + + const { getByRole, getByText } = await renderInTestApp( + + + , + ); + + // Open the Autocomplete dropdown + const input = getByRole('textbox'); + await userEvent.click(input); + + // Verify that the available owners are shown + await waitFor(() => expect(getByText('owner1')).toBeInTheDocument()); + + // Verify that selecting an option calls onChange + await userEvent.click(getByText('owner1')); + expect(onChange).toHaveBeenCalledWith({ + owner: 'owner1', + }); + }); + + it('should filter out excluded owners', async () => { + const onChange = jest.fn(); + + const { getByRole, getByText } = await renderInTestApp( + + + , + ); + + // Open the Autocomplete dropdown + const input = getByRole('textbox'); + await userEvent.click(input); + + // Verify that the excluded owners are not shown + await waitFor(() => expect(getByText('owner2')).toBeInTheDocument()); + expect(screen.queryByText('owner1')).not.toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoOwnerPicker/GitLabRepoOwnerPicker.tsx b/plugins/scaffolder/src/components/fields/RepoOwnerPicker/GitLabRepoOwnerPicker.tsx new file mode 100644 index 0000000000..8bc626c0a5 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoOwnerPicker/GitLabRepoOwnerPicker.tsx @@ -0,0 +1,113 @@ +/* + * Copyright 2025 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 { useCallback, useState } from 'react'; +import useDebounce from 'react-use/esm/useDebounce'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; + +import { BaseRepoOwnerPickerProps } from './types'; +import { scaffolderTranslationRef } from '../../../translation'; + +/** + * The underlying component that is rendered in the form for the `GitLabRepoOwnerPicker` + * field extension. + * + * @public + * + */ +export const GitLabRepoOwnerPicker = ({ + onChange, + state, + rawErrors, + accessToken, + isDisabled, + required, + schema, + excludedOwners = [], +}: BaseRepoOwnerPickerProps<{ + accessToken?: string; + excludedOwners?: string[]; +}>) => { + const { host, owner } = state; + + const [availableOwners, setAvailableOwners] = useState([]); + + const scaffolderApi = useApi(scaffolderApiRef); + const { t } = useTranslationRef(scaffolderTranslationRef); + + const updateAvailableOwners = useCallback(() => { + if (!scaffolderApi.autocomplete || !accessToken || !host) { + setAvailableOwners([]); + return; + } + + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'groups', + provider: 'gitlab', + context: { host }, + }) + .then(({ results }) => { + const owners = results + .map(r => r.id) + .filter(id => !excludedOwners.includes(id)); + + setAvailableOwners(owners); + }) + .catch(() => { + setAvailableOwners([]); + }); + }, [host, accessToken, scaffolderApi, excludedOwners]); + + useDebounce(updateAvailableOwners, 500, [updateAvailableOwners]); + + return ( + 0 && !owner} + > + { + onChange({ owner: newValue || '' }); + }} + disabled={isDisabled} + options={availableOwners} + renderInput={params => ( + + )} + freeSolo + autoSelect + /> + + {schema?.description ?? t('fields.repoOwnerPicker.description')} + + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoOwnerPicker/RepoOwnerPicker.tsx b/plugins/scaffolder/src/components/fields/RepoOwnerPicker/RepoOwnerPicker.tsx index bcb1a0001c..ad88b3a02a 100644 --- a/plugins/scaffolder/src/components/fields/RepoOwnerPicker/RepoOwnerPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoOwnerPicker/RepoOwnerPicker.tsx @@ -27,6 +27,7 @@ import { RepoOwnerPickerProps } from './schema'; import { RepoOwnerPickerState } from './types'; import { DefaultRepoOwnerPicker } from './DefaultRepoOwnerPicker'; import { GitHubRepoOwnerPicker } from './GitHubRepoOwnerPicker'; +import { GitLabRepoOwnerPicker } from './GitLabRepoOwnerPicker'; /** * The underlying component that is rendered in the form for the `RepoOwnerPicker` @@ -120,6 +121,22 @@ export const RepoOwnerPicker = (props: RepoOwnerPickerProps) => { excludedOwners={excludedOwners} /> ); + case 'gitlab': + return ( + + ); default: return (