diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index b4a3deea54..c79f98c780 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -229,4 +229,11 @@ export interface ScaffolderApi { streamLogs(options: ScaffolderStreamLogsOptions): Observable; dryRun?(options: ScaffolderDryRunOptions): Promise; + + autocomplete( + token: string, + provider: string, + resource: string, + params?: Record, + ): Promise; } diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx index 27fde2083f..3a2aa1469f 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx @@ -36,6 +36,7 @@ const scaffolderApiMock: jest.Mocked = { streamLogs: jest.fn(), listActions: jest.fn(), listTasks: jest.fn(), + autocomplete: jest.fn(), }; const catalogApiMock: jest.Mocked = { getEntityByRef: jest.fn(), diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index a17fc14105..d1b58c8eca 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -67,7 +67,6 @@ "@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/api.ts b/plugins/scaffolder/src/api.ts index 6ef5fff588..ff5c89e91e 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -337,4 +337,28 @@ export class ScaffolderClient implements ScaffolderApi { return await response.json(); } + + async autocomplete( + token: string, + provider: string, + resource: string, + params?: Record, + ): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); + const query = new URLSearchParams({ + ...params, + token, + }); + const url = `${baseUrl}/v2/autocomplete/${provider}/${resource}?${query}`; + + const response = await this.fetchApi.fetch(url, { + method: 'GET', + }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return await response.json(); + } } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx index 9dae5b24a6..404fb6ddf6 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx @@ -17,23 +17,33 @@ import React from 'react'; import { BitbucketRepoPicker } from './BitbucketRepoPicker'; import { render, fireEvent, waitFor } from '@testing-library/react'; -import { setupServer } from 'msw/node'; -import { rest } from 'msw'; -import { Models } from '@backstage/plugin-bitbucket-cloud-common'; import userEvent from '@testing-library/user-event'; - -const server = setupServer(); +import { TestApiProvider } from '@backstage/test-utils'; +import { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; describe('BitbucketRepoPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest + .fn() + .mockImplementation((_token, _provider, resource) => [ + `${resource}_example`, + ]), + }; + it('renders a select if there is a list of allowed owners', async () => { const allowedOwners = ['owner1', 'owner2']; const { findByText } = render( - , + + + , ); expect(await findByText('owner1')).toBeInTheDocument(); @@ -44,7 +54,13 @@ describe('BitbucketRepoPicker', () => { const state = { host: 'bitbucket.org', workspace: 'lolsWorkspace' }; const { getAllByRole } = render( - , + + + , ); expect(getAllByRole('textbox')).toHaveLength(2); @@ -57,7 +73,13 @@ describe('BitbucketRepoPicker', () => { }; const { getAllByRole } = render( - , + + + , ); expect(getAllByRole('textbox')).toHaveLength(1); @@ -67,11 +89,13 @@ describe('BitbucketRepoPicker', () => { it('calls onChange when the workspace changes', () => { const onChange = jest.fn(); const { getAllByRole } = render( - , + + + , ); const workspaceInput = getAllByRole('textbox')[0]; @@ -86,11 +110,13 @@ describe('BitbucketRepoPicker', () => { it('calls onChange when the project changes', () => { const onChange = jest.fn(); const { getAllByRole } = render( - , + + + , ); const projectInput = getAllByRole('textbox')[1]; @@ -102,11 +128,13 @@ describe('BitbucketRepoPicker', () => { it('Does not render a select if the list of allowed projects does not exist', async () => { const { getAllByRole } = render( - , + + + , ); expect(getAllByRole('textbox')).toHaveLength(2); @@ -115,12 +143,14 @@ describe('BitbucketRepoPicker', () => { it('Does not render a select if the list of allowed projects is empty', async () => { const { getAllByRole } = render( - , + + + , ); expect(getAllByRole('textbox')).toHaveLength(2); @@ -130,12 +160,14 @@ describe('BitbucketRepoPicker', () => { 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(); @@ -144,71 +176,17 @@ describe('BitbucketRepoPicker', () => { }); describe('autocompletion', () => { - beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); - beforeEach(() => { - // BitbucketCloudClient.listWorkspaces() - server.use( - rest.get('https://api.bitbucket.org/2.0/workspaces', (_, res, ctx) => { - const response = { - values: [ - { - type: 'workspace', - slug: 'workspace1', - } as Models.Workspace, - ], - }; - return res(ctx.json(response)); - }), - ); - - // BitbucketCloudClient.listProjectsByWorkspace() - server.use( - rest.get( - 'https://api.bitbucket.org/2.0/workspaces/workspace1/projects', - (_, res, ctx) => { - const response = { - values: [ - { - type: 'project', - key: 'project1', - } as Models.Project, - ], - }; - return res(ctx.json(response)); - }, - ), - ); - - // BitbucketCloudClient.listRepositoriesByWorkspace() - server.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/workspace1', - (_, res, ctx) => { - const response = { - values: [ - { - type: 'repository', - slug: 'repo1', - } as Models.Repository, - ], - }; - return res(ctx.json(response)); - }, - ), - ); - }); - afterAll(() => server.close()); - afterEach(() => server.resetHandlers()); - it('should populate workspaces if host is set and accessToken is provided', async () => { const onChange = jest.fn(); const { getAllByRole, getByText } = render( - , + + + , ); // Open the Autcomplete dropdown @@ -216,22 +194,28 @@ describe('BitbucketRepoPicker', () => { await userEvent.click(workspaceInput); // Verify that the available workspaces are shown - await waitFor(() => expect(getByText('workspace1')).toBeInTheDocument()); + await waitFor(() => + expect(getByText('workspaces_example')).toBeInTheDocument(), + ); // Verify that selecting an option calls onChange - await userEvent.click(getByText('workspace1')); - expect(onChange).toHaveBeenCalledWith({ workspace: 'workspace1' }); + await userEvent.click(getByText('workspaces_example')); + expect(onChange).toHaveBeenCalledWith({ + workspace: 'workspaces_example', + }); }); it('should populate projects if host and workspace are set and accessToken is provided', async () => { const onChange = jest.fn(); const { getAllByRole, getByText } = render( - , + + + , ); // Open the Autcomplete dropdown @@ -239,31 +223,37 @@ describe('BitbucketRepoPicker', () => { await userEvent.click(projectInput); // Verify that the available projects are shown - await waitFor(() => expect(getByText('project1')).toBeInTheDocument()); + await waitFor(() => + expect(getByText('projects_example')).toBeInTheDocument(), + ); // Verify that selecting an option calls onChange - await userEvent.click(getByText('project1')); - expect(onChange).toHaveBeenCalledWith({ project: 'project1' }); + await userEvent.click(getByText('projects_example')); + expect(onChange).toHaveBeenCalledWith({ project: 'projects_example' }); }); it('should populate repositories if host, workspace and project are set and accessToken is provided', async () => { const onChange = jest.fn(); render( - , + + + , ); // Verify that the available repos are updated await waitFor(() => - expect(onChange).toHaveBeenCalledWith({ availableRepos: ['repo1'] }), + expect(onChange).toHaveBeenCalledWith({ + availableRepos: ['repositories_example'], + }), ); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 72c39fb11c..995470fbcb 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -18,10 +18,11 @@ import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import { Select, SelectItem } from '@backstage/core-components'; import { RepoUrlPickerState } from './types'; -import { BitbucketCloudClient } from '@backstage/plugin-bitbucket-cloud-common'; import Autocomplete from '@material-ui/lab/Autocomplete'; import TextField from '@material-ui/core/TextField'; import useDebounce from 'react-use/esm/useDebounce'; +import { useApi } from '@backstage/core-plugin-api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; /** * The underlying component that is rendered in the form for the `BitbucketRepoPicker` @@ -62,35 +63,21 @@ export const BitbucketRepoPicker = (props: { } }, [allowedOwners, host, onChange]); - const [client, setClient] = useState(); + const scaffolderApi = useApi(scaffolderApiRef); + const [availableWorkspaces, setAvailableWorkspaces] = useState([]); const [availableProjects, setAvailableProjects] = useState([]); - useEffect(() => { - if (host === 'bitbucket.org' && accessToken) { - setClient( - BitbucketCloudClient.fromConfig({ - host, - apiBaseUrl: 'https://api.bitbucket.org/2.0', - accessToken, - }), - ); - } else { - setClient(undefined); - } - }, [host, accessToken]); - // Update available workspaces when client is available useDebounce( () => { const updateAvailableWorkspaces = async () => { - if (client) { - const result: string[] = []; - - for await (const page of client.listWorkspaces().iteratePages()) { - const keys = [...page.values!].map(p => p.slug!); - result.push(...keys); - } + if (host === 'bitbucket.org' && accessToken) { + const result = await scaffolderApi.autocomplete( + accessToken, + 'bitbucketCloud', + 'workspaces', + ); setAvailableWorkspaces(result); } else { @@ -101,22 +88,20 @@ export const BitbucketRepoPicker = (props: { updateAvailableWorkspaces().catch(() => setAvailableWorkspaces([])); }, 500, - [client], + [host, accessToken], ); // Update available projects when client is available and workspace changes useDebounce( () => { const updateAvailableProjects = async () => { - if (client && workspace) { - const result: string[] = []; - - for await (const page of client - .listProjectsByWorkspace(workspace) - .iteratePages()) { - const keys = [...page.values!].map(p => p.key!); - result.push(...keys); - } + if (host === 'bitbucket.org' && accessToken && workspace) { + const result = await scaffolderApi.autocomplete( + accessToken, + 'bitbucketCloud', + 'projects', + { workspace }, + ); setAvailableProjects(result); } else { @@ -127,24 +112,20 @@ export const BitbucketRepoPicker = (props: { updateAvailableProjects().catch(() => setAvailableProjects([])); }, 500, - [client, workspace], + [host, accessToken, workspace], ); // Update available repositories when client is available and workspace or project changes useDebounce( () => { const updateAvailableRepositories = async () => { - if (client && workspace && project) { - const availableRepos: string[] = []; - - for await (const page of client - .listRepositoriesByWorkspace(workspace, { - q: `project.key="${project}"`, - }) - .iteratePages()) { - const keys = [...page.values!].map(p => p.slug!); - availableRepos.push(...keys); - } + if (host === 'bitbucket.org' && accessToken && workspace && project) { + const availableRepos = await scaffolderApi.autocomplete( + accessToken, + 'bitbucketCloud', + 'repositories', + { workspace, project }, + ); onChange({ availableRepos }); } else { @@ -157,7 +138,7 @@ export const BitbucketRepoPicker = (props: { ); }, 500, - [client, workspace, project, onChange], + [host, accessToken, workspace, project], ); return ( diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx index 269ff81271..2639e8149c 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -51,6 +51,7 @@ const scaffolderApiMock: jest.Mocked = { streamLogs: jest.fn(), listActions: jest.fn(), listTasks: jest.fn(), + autocomplete: jest.fn(), }; const catalogApiMock: jest.Mocked = { diff --git a/yarn.lock b/yarn.lock index 7f44f1559c..f81ce9dabd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7269,7 +7269,6 @@ __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:^"