feat: use autocomplete endpoint in frontend
Signed-off-by: Benjamin Janssens <benji.janssens@gmail.com>
This commit is contained in:
@@ -229,4 +229,11 @@ export interface ScaffolderApi {
|
||||
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
|
||||
|
||||
dryRun?(options: ScaffolderDryRunOptions): Promise<ScaffolderDryRunResponse>;
|
||||
|
||||
autocomplete(
|
||||
token: string,
|
||||
provider: string,
|
||||
resource: string,
|
||||
params?: Record<string, string>,
|
||||
): Promise<string[]>;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
streamLogs: jest.fn(),
|
||||
listActions: jest.fn(),
|
||||
listTasks: jest.fn(),
|
||||
autocomplete: jest.fn(),
|
||||
};
|
||||
const catalogApiMock: jest.Mocked<CatalogApi> = {
|
||||
getEntityByRef: jest.fn(),
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -337,4 +337,28 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async autocomplete(
|
||||
token: string,
|
||||
provider: string,
|
||||
resource: string,
|
||||
params?: Record<string, string>,
|
||||
): Promise<string[]> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
+115
-125
@@ -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<ScaffolderApi> = {
|
||||
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(
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(await findByText('owner1')).toBeInTheDocument();
|
||||
@@ -44,7 +54,13 @@ describe('BitbucketRepoPicker', () => {
|
||||
const state = { host: 'bitbucket.org', workspace: 'lolsWorkspace' };
|
||||
|
||||
const { getAllByRole } = render(
|
||||
<BitbucketRepoPicker onChange={jest.fn()} rawErrors={[]} state={state} />,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={state}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(getAllByRole('textbox')).toHaveLength(2);
|
||||
@@ -57,7 +73,13 @@ describe('BitbucketRepoPicker', () => {
|
||||
};
|
||||
|
||||
const { getAllByRole } = render(
|
||||
<BitbucketRepoPicker onChange={jest.fn()} rawErrors={[]} state={state} />,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={state}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org' }}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org' }}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org' }}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org' }}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', repoName: 'repo' }}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', repoName: 'repo' }}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', repoName: 'repo' }}
|
||||
allowedProjects={[]}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', repoName: 'repo' }}
|
||||
allowedProjects={[]}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', repoName: 'repo' }}
|
||||
allowedProjects={allowedProjects}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', repoName: 'repo' }}
|
||||
allowedProjects={allowedProjects}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org' }}
|
||||
accessToken="foo"
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org' }}
|
||||
accessToken="foo"
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
// 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(
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', workspace: 'workspace1' }}
|
||||
accessToken="foo"
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org', workspace: 'workspace1' }}
|
||||
accessToken="foo"
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
// 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(
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{
|
||||
host: 'bitbucket.org',
|
||||
workspace: 'workspace1',
|
||||
project: 'project1',
|
||||
}}
|
||||
accessToken="foo"
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{
|
||||
host: 'bitbucket.org',
|
||||
workspace: 'workspace1',
|
||||
project: 'project1',
|
||||
}}
|
||||
accessToken="foo"
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
// Verify that the available repos are updated
|
||||
await waitFor(() =>
|
||||
expect(onChange).toHaveBeenCalledWith({ availableRepos: ['repo1'] }),
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
availableRepos: ['repositories_example'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<BitbucketCloudClient>();
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
|
||||
const [availableWorkspaces, setAvailableWorkspaces] = useState<string[]>([]);
|
||||
const [availableProjects, setAvailableProjects] = useState<string[]>([]);
|
||||
|
||||
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 (
|
||||
|
||||
@@ -51,6 +51,7 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
streamLogs: jest.fn(),
|
||||
listActions: jest.fn(),
|
||||
listTasks: jest.fn(),
|
||||
autocomplete: jest.fn(),
|
||||
};
|
||||
|
||||
const catalogApiMock: jest.Mocked<CatalogApi> = {
|
||||
|
||||
@@ -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:^"
|
||||
|
||||
Reference in New Issue
Block a user