Merge pull request #25627 from benjidotsh/scaffolder/repo-url-picker-cleanup
refactor(scaffolder): clean up codebase of `RepoUrlPicker`
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Cleaned up codebase of RepoUrlPicker
|
||||
@@ -19,16 +19,15 @@ import FormControl from '@material-ui/core/FormControl';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
import { BaseRepoUrlPickerProps } from './types';
|
||||
import { Select, SelectItem } from '@backstage/core-components';
|
||||
|
||||
export const AzureRepoPicker = (props: {
|
||||
allowedOrganizations?: string[];
|
||||
allowedProject?: string[];
|
||||
rawErrors: string[];
|
||||
state: RepoUrlPickerState;
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
}) => {
|
||||
export const AzureRepoPicker = (
|
||||
props: BaseRepoUrlPickerProps<{
|
||||
allowedOrganizations?: string[];
|
||||
allowedProject?: string[];
|
||||
}>,
|
||||
) => {
|
||||
const {
|
||||
allowedOrganizations = [],
|
||||
allowedProject = [],
|
||||
|
||||
@@ -27,9 +27,11 @@ import { act } from 'react-dom/test-utils';
|
||||
|
||||
describe('BitbucketRepoPicker', () => {
|
||||
const scaffolderApiMock: Partial<ScaffolderApi> = {
|
||||
autocomplete: jest.fn().mockImplementation(opts => ({
|
||||
results: [{ title: `${opts.resource}_example` }],
|
||||
})),
|
||||
autocomplete: jest.fn().mockImplementation(opts =>
|
||||
Promise.resolve({
|
||||
results: [{ title: `${opts.resource}_example` }],
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
it('renders a select if there is a list of allowed owners', async () => {
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
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 { BaseRepoUrlPickerProps } from './types';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import useDebounce from 'react-use/esm/useDebounce';
|
||||
@@ -33,14 +33,13 @@ import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
|
||||
* @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[];
|
||||
accessToken?: string;
|
||||
}) => {
|
||||
export const BitbucketRepoPicker = (
|
||||
props: BaseRepoUrlPickerProps<{
|
||||
allowedOwners?: string[];
|
||||
allowedProjects?: string[];
|
||||
accessToken?: string;
|
||||
}>,
|
||||
) => {
|
||||
const {
|
||||
allowedOwners = [],
|
||||
allowedProjects = [],
|
||||
@@ -69,93 +68,90 @@ export const BitbucketRepoPicker = (props: {
|
||||
const [availableProjects, setAvailableProjects] = useState<string[]>([]);
|
||||
|
||||
// Update available workspaces when client is available
|
||||
useDebounce(
|
||||
() => {
|
||||
const updateAvailableWorkspaces = async () => {
|
||||
if (
|
||||
host === 'bitbucket.org' &&
|
||||
accessToken &&
|
||||
scaffolderApi.autocomplete
|
||||
) {
|
||||
const { results } = await scaffolderApi.autocomplete({
|
||||
token: accessToken,
|
||||
resource: 'workspaces',
|
||||
context: {},
|
||||
provider: 'bitbucket-cloud',
|
||||
});
|
||||
const updateAvailableWorkspaces = useCallback(() => {
|
||||
if (
|
||||
!scaffolderApi.autocomplete ||
|
||||
!accessToken ||
|
||||
host !== 'bitbucket.org'
|
||||
) {
|
||||
setAvailableWorkspaces([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setAvailableWorkspaces(results.map(r => r.title));
|
||||
} else {
|
||||
setAvailableWorkspaces([]);
|
||||
}
|
||||
};
|
||||
scaffolderApi
|
||||
.autocomplete({
|
||||
token: accessToken,
|
||||
resource: 'workspaces',
|
||||
provider: 'bitbucket-cloud',
|
||||
})
|
||||
.then(({ results }) => {
|
||||
setAvailableWorkspaces(results.map(r => r.title));
|
||||
})
|
||||
.catch(() => {
|
||||
setAvailableWorkspaces([]);
|
||||
});
|
||||
}, [scaffolderApi, accessToken, host]);
|
||||
|
||||
updateAvailableWorkspaces().catch(() => setAvailableWorkspaces([]));
|
||||
},
|
||||
500,
|
||||
[host, accessToken],
|
||||
);
|
||||
useDebounce(updateAvailableWorkspaces, 500, [updateAvailableWorkspaces]);
|
||||
|
||||
// Update available projects when client is available and workspace changes
|
||||
useDebounce(
|
||||
() => {
|
||||
const updateAvailableProjects = async () => {
|
||||
if (
|
||||
host === 'bitbucket.org' &&
|
||||
accessToken &&
|
||||
workspace &&
|
||||
scaffolderApi.autocomplete
|
||||
) {
|
||||
const { results } = await scaffolderApi.autocomplete({
|
||||
token: accessToken,
|
||||
resource: 'projects',
|
||||
context: { workspace },
|
||||
provider: 'bitbucket-cloud',
|
||||
});
|
||||
const updateAvailableProjects = useCallback(() => {
|
||||
if (
|
||||
!scaffolderApi.autocomplete ||
|
||||
!accessToken ||
|
||||
host !== 'bitbucket.org' ||
|
||||
!workspace
|
||||
) {
|
||||
setAvailableProjects([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setAvailableProjects(results.map(r => r.title));
|
||||
} else {
|
||||
setAvailableProjects([]);
|
||||
}
|
||||
};
|
||||
scaffolderApi
|
||||
.autocomplete({
|
||||
token: accessToken,
|
||||
resource: 'projects',
|
||||
context: { workspace },
|
||||
provider: 'bitbucket-cloud',
|
||||
})
|
||||
.then(({ results }) => {
|
||||
setAvailableProjects(results.map(r => r.title));
|
||||
})
|
||||
.catch(() => {
|
||||
setAvailableProjects([]);
|
||||
});
|
||||
}, [scaffolderApi, accessToken, host, workspace]);
|
||||
|
||||
updateAvailableProjects().catch(() => setAvailableProjects([]));
|
||||
},
|
||||
500,
|
||||
[host, accessToken, workspace],
|
||||
);
|
||||
useDebounce(updateAvailableProjects, 500, [updateAvailableProjects]);
|
||||
|
||||
// Update available repositories when client is available and workspace or project changes
|
||||
useDebounce(
|
||||
() => {
|
||||
const updateAvailableRepositories = async () => {
|
||||
if (
|
||||
host === 'bitbucket.org' &&
|
||||
accessToken &&
|
||||
workspace &&
|
||||
project &&
|
||||
scaffolderApi.autocomplete
|
||||
) {
|
||||
const { results } = await scaffolderApi.autocomplete({
|
||||
token: accessToken,
|
||||
resource: 'repositories',
|
||||
context: { workspace, project },
|
||||
provider: 'bitbucket-cloud',
|
||||
});
|
||||
const updateAvailableRepositories = useCallback(() => {
|
||||
if (
|
||||
!scaffolderApi.autocomplete ||
|
||||
!accessToken ||
|
||||
host !== 'bitbucket.org' ||
|
||||
!workspace ||
|
||||
!project
|
||||
) {
|
||||
onChange({ availableRepos: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({ availableRepos: results.map(r => r.title) });
|
||||
} else {
|
||||
onChange({ availableRepos: [] });
|
||||
}
|
||||
};
|
||||
scaffolderApi
|
||||
.autocomplete({
|
||||
token: accessToken,
|
||||
resource: 'repositories',
|
||||
context: { workspace, project },
|
||||
provider: 'bitbucket-cloud',
|
||||
})
|
||||
.then(({ results }) => {
|
||||
onChange({ availableRepos: results.map(r => r.title) });
|
||||
})
|
||||
.catch(() => {
|
||||
onChange({ availableRepos: [] });
|
||||
});
|
||||
}, [scaffolderApi, accessToken, host, workspace, project, onChange]);
|
||||
|
||||
updateAvailableRepositories().catch(() =>
|
||||
onChange({ availableRepos: [] }),
|
||||
);
|
||||
},
|
||||
500,
|
||||
[host, accessToken, workspace, project],
|
||||
);
|
||||
useDebounce(updateAvailableRepositories, 500, [updateAvailableRepositories]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -18,13 +18,9 @@ import FormControl from '@material-ui/core/FormControl';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
import { BaseRepoUrlPickerProps } from './types';
|
||||
|
||||
export const GerritRepoPicker = (props: {
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
state: RepoUrlPickerState;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
export const GerritRepoPicker = (props: BaseRepoUrlPickerProps) => {
|
||||
const { onChange, rawErrors, state } = props;
|
||||
const { workspace, owner } = state;
|
||||
return (
|
||||
|
||||
@@ -19,15 +19,14 @@ import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import { Select, SelectItem } from '@backstage/core-components';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
import { BaseRepoUrlPickerProps } from './types';
|
||||
|
||||
export const GiteaRepoPicker = (props: {
|
||||
allowedOwners?: string[];
|
||||
allowedRepos?: string[];
|
||||
state: RepoUrlPickerState;
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
export const GiteaRepoPicker = (
|
||||
props: BaseRepoUrlPickerProps<{
|
||||
allowedOwners?: string[];
|
||||
allowedRepos?: string[];
|
||||
}>,
|
||||
) => {
|
||||
const { allowedOwners = [], state, onChange, rawErrors } = props;
|
||||
const ownerItems: SelectItem[] = allowedOwners
|
||||
? allowedOwners.map(i => ({ label: i, value: i }))
|
||||
|
||||
@@ -19,14 +19,13 @@ import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import { Select, SelectItem } from '@backstage/core-components';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
import { BaseRepoUrlPickerProps } from './types';
|
||||
|
||||
export const GithubRepoPicker = (props: {
|
||||
allowedOwners?: string[];
|
||||
rawErrors: string[];
|
||||
state: RepoUrlPickerState;
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
}) => {
|
||||
export const GithubRepoPicker = (
|
||||
props: BaseRepoUrlPickerProps<{
|
||||
allowedOwners?: string[];
|
||||
}>,
|
||||
) => {
|
||||
const { allowedOwners = [], rawErrors, state, onChange } = props;
|
||||
const ownerItems: SelectItem[] = allowedOwners
|
||||
? allowedOwners.map(i => ({ label: i, value: i }))
|
||||
|
||||
@@ -19,15 +19,14 @@ import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import { Select, SelectItem } from '@backstage/core-components';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
import { BaseRepoUrlPickerProps } from './types';
|
||||
|
||||
export const GitlabRepoPicker = (props: {
|
||||
allowedOwners?: string[];
|
||||
allowedRepos?: string[];
|
||||
state: RepoUrlPickerState;
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
export const GitlabRepoPicker = (
|
||||
props: BaseRepoUrlPickerProps<{
|
||||
allowedOwners?: string[];
|
||||
allowedRepos?: string[];
|
||||
}>,
|
||||
) => {
|
||||
const { allowedOwners = [], state, onChange, rawErrors } = props;
|
||||
const ownerItems: SelectItem[] = allowedOwners
|
||||
? allowedOwners.map(i => ({ label: i, value: i }))
|
||||
|
||||
@@ -215,13 +215,14 @@ describe('RepoUrlPicker', () => {
|
||||
|
||||
describe('requestUserCredentials', () => {
|
||||
it('should call the scmAuthApi with the correct params', async () => {
|
||||
const secretsKey = 'testKey';
|
||||
|
||||
const SecretsComponent = () => {
|
||||
const { secrets } = useTemplateSecrets();
|
||||
return (
|
||||
<div data-testid="current-secrets">{JSON.stringify({ secrets })}</div>
|
||||
);
|
||||
const secret = secrets[secretsKey];
|
||||
return secret ? <div>{secret}</div> : null;
|
||||
};
|
||||
const { getByTestId } = await renderInTestApp(
|
||||
const { getByText } = await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[scmIntegrationsApiRef, mockIntegrationsApi],
|
||||
@@ -237,7 +238,7 @@ describe('RepoUrlPicker', () => {
|
||||
'ui:field': 'RepoUrlPicker',
|
||||
'ui:options': {
|
||||
requestUserCredentials: {
|
||||
secretsKey: 'testKey',
|
||||
secretsKey,
|
||||
additionalScopes: { github: ['workflow'] },
|
||||
},
|
||||
},
|
||||
@@ -266,21 +267,9 @@ describe('RepoUrlPicker', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const currentSecrets = JSON.parse(
|
||||
getByTestId('current-secrets').textContent!,
|
||||
);
|
||||
|
||||
expect(currentSecrets).toEqual({
|
||||
secrets: { testKey: 'abc123' },
|
||||
});
|
||||
expect(getByText('abc123')).toBeInTheDocument();
|
||||
});
|
||||
it('should call the scmAuthApi with the correct params if workspace is nested', async () => {
|
||||
const SecretsComponent = () => {
|
||||
const { secrets } = useTemplateSecrets();
|
||||
return (
|
||||
<div data-testid="current-secrets">{JSON.stringify({ secrets })}</div>
|
||||
);
|
||||
};
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
@@ -306,7 +295,6 @@ describe('RepoUrlPicker', () => {
|
||||
RepoUrlPicker: RepoUrlPicker as ScaffolderRJSFField<string>,
|
||||
}}
|
||||
/>
|
||||
<SecretsComponent />
|
||||
</SecretsContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
@@ -325,11 +313,12 @@ describe('RepoUrlPicker', () => {
|
||||
});
|
||||
|
||||
it('should call the scmAuthApi with the new host when the host is changed', async () => {
|
||||
const secretsKey = 'testKey';
|
||||
|
||||
const SecretsComponent = () => {
|
||||
const { secrets } = useTemplateSecrets();
|
||||
return (
|
||||
<div data-testid="current-secrets">{JSON.stringify({ secrets })}</div>
|
||||
);
|
||||
const secret = secrets[secretsKey];
|
||||
return secret ? <div>{secret}</div> : null;
|
||||
};
|
||||
const allowedHosts = ['github.com', 'gitlab.example.com'];
|
||||
|
||||
@@ -346,7 +335,7 @@ describe('RepoUrlPicker', () => {
|
||||
);
|
||||
const secondHost = allowedHosts[1];
|
||||
|
||||
const { getAllByRole, getByTestId } = await renderInTestApp(
|
||||
const { getAllByRole, getByText } = await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[scmIntegrationsApiRef, mockIntegrationsApi],
|
||||
@@ -363,7 +352,7 @@ describe('RepoUrlPicker', () => {
|
||||
'ui:options': {
|
||||
allowedHosts,
|
||||
requestUserCredentials: {
|
||||
secretsKey: 'testKey',
|
||||
secretsKey,
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -380,12 +369,7 @@ describe('RepoUrlPicker', () => {
|
||||
// need to wait for the debounce to finish to fetch credentials for the first selected host
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
});
|
||||
const firstHostSecrets = JSON.parse(
|
||||
getByTestId('current-secrets').textContent!,
|
||||
);
|
||||
expect(firstHostSecrets).toEqual({
|
||||
secrets: { testKey: 'abc123' },
|
||||
});
|
||||
expect(getByText('abc123')).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
// Select the second host
|
||||
@@ -395,12 +379,7 @@ describe('RepoUrlPicker', () => {
|
||||
// need to wait for the debounce to finish
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
});
|
||||
const secondHostSecrets = JSON.parse(
|
||||
getByTestId('current-secrets').textContent!,
|
||||
);
|
||||
expect(secondHostSecrets).toEqual({
|
||||
secrets: { testKey: 'def456' },
|
||||
});
|
||||
expect(getByText('def456')).toBeInTheDocument();
|
||||
expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({
|
||||
url: `https://${secondHost}`,
|
||||
additionalScope: {
|
||||
|
||||
@@ -22,3 +22,9 @@ export interface RepoUrlPickerState {
|
||||
project?: string;
|
||||
availableRepos?: string[];
|
||||
}
|
||||
|
||||
export type BaseRepoUrlPickerProps<T extends {} = {}> = T & {
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
state: RepoUrlPickerState;
|
||||
rawErrors: string[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user