Merge pull request #11806 from dotressel/add-allowed-repos-to-repourlpicker
Scaffolder: RepoUrlPicker: Add allowedRepos Option and move RepoName Input to own Component
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Add `allowedRepos` `ui:option` to `RepoUrlPicker` component, and move `repoName` field to own component
|
||||
@@ -320,6 +320,30 @@ The `allowedHosts` part should be set to where you wish to enable this template
|
||||
to publish to. And it can be any host that is listed in your `integrations`
|
||||
config in `app-config.yaml`.
|
||||
|
||||
Besides specifying `allowedHosts` you can also restrict the template to publish to
|
||||
repositories owned by specific users/groups/namespaces by setting the `allowedOwners`
|
||||
option. With the `allowedRepos` option you are able to narrow it down further to a
|
||||
specific set of repository names. A full example could look like this:
|
||||
|
||||
```yaml
|
||||
- title: Choose a location
|
||||
required:
|
||||
- repoUrl
|
||||
properties:
|
||||
repoUrl:
|
||||
title: Repository Location
|
||||
type: string
|
||||
ui:field: RepoUrlPicker
|
||||
ui:options:
|
||||
allowedHosts:
|
||||
- github.com
|
||||
allowedOwners:
|
||||
- backstage
|
||||
- someGithubUser
|
||||
allowedRepos:
|
||||
- backstage
|
||||
```
|
||||
|
||||
The `RepoUrlPicker` is a custom field that we provide part of the
|
||||
`plugin-scaffolder`. You can provide your own custom fields by
|
||||
[writing your own Custom Field Extensions](./writing-custom-field-extensions.md)
|
||||
|
||||
@@ -195,6 +195,8 @@ export interface RepoUrlPickerUiOptions {
|
||||
// (undocumented)
|
||||
allowedOwners?: string[];
|
||||
// (undocumented)
|
||||
allowedRepos?: string[];
|
||||
// (undocumented)
|
||||
requestUserCredentials?: {
|
||||
secretsKey: string;
|
||||
additionalScopes?: {
|
||||
|
||||
@@ -19,14 +19,14 @@ import { AzureRepoPicker } from './AzureRepoPicker';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
describe('AzureRepoPicker', () => {
|
||||
it('renders the three input fields', async () => {
|
||||
it('renders the two input fields', async () => {
|
||||
const { getAllByRole } = render(
|
||||
<AzureRepoPicker onChange={jest.fn()} rawErrors={[]} state={{}} />,
|
||||
);
|
||||
|
||||
const allInputs = getAllByRole('textbox');
|
||||
|
||||
expect(allInputs).toHaveLength(3);
|
||||
expect(allInputs).toHaveLength(2);
|
||||
});
|
||||
|
||||
describe('org field', () => {
|
||||
@@ -58,19 +58,4 @@ describe('AzureRepoPicker', () => {
|
||||
expect(onChange).toHaveBeenCalledWith({ owner: 'owner' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('repoName field', () => {
|
||||
it('calls onChange when the repoName changes', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<AzureRepoPicker onChange={onChange} rawErrors={[]} state={{}} />,
|
||||
);
|
||||
|
||||
const repoNameInput = getAllByRole('textbox')[2];
|
||||
|
||||
fireEvent.change(repoNameInput, { target: { value: 'repoName' } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ repoName: 'repoName' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ export const AzureRepoPicker = (props: {
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { rawErrors, state, onChange } = props;
|
||||
const { organization, repoName, owner } = state;
|
||||
const { organization, owner } = state;
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
@@ -58,19 +58,6 @@ export const AzureRepoPicker = (props: {
|
||||
/>
|
||||
<FormHelperText>The Owner that this repo will belong to</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !repoName}
|
||||
>
|
||||
<InputLabel htmlFor="repoNameInput">Repository</InputLabel>
|
||||
<Input
|
||||
id="repoNameInput"
|
||||
onChange={e => onChange({ repoName: e.target.value })}
|
||||
value={repoName}
|
||||
/>
|
||||
<FormHelperText>The name of the repository</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-21
@@ -26,7 +26,7 @@ describe('BitbucketRepoPicker', () => {
|
||||
<BitbucketRepoPicker onChange={jest.fn()} rawErrors={[]} state={state} />,
|
||||
);
|
||||
|
||||
expect(getAllByRole('textbox')).toHaveLength(3);
|
||||
expect(getAllByRole('textbox')).toHaveLength(2);
|
||||
expect(getAllByRole('textbox')[0]).toHaveValue('lolsWorkspace');
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('BitbucketRepoPicker', () => {
|
||||
<BitbucketRepoPicker onChange={jest.fn()} rawErrors={[]} state={state} />,
|
||||
);
|
||||
|
||||
expect(getAllByRole('textbox')).toHaveLength(2);
|
||||
expect(getAllByRole('textbox')).toHaveLength(1);
|
||||
});
|
||||
describe('workspace field', () => {
|
||||
it('calls onChange when the workspace changes', () => {
|
||||
@@ -78,23 +78,4 @@ describe('BitbucketRepoPicker', () => {
|
||||
expect(onChange).toHaveBeenCalledWith({ project: 'test-project' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('repoName field', () => {
|
||||
it('calls onChange when the repoName changes', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const repoNameInput = getAllByRole('textbox')[2];
|
||||
|
||||
fireEvent.change(repoNameInput, { target: { value: 'test-repo' } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ repoName: 'test-repo' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export const BitbucketRepoPicker = (props: {
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { onChange, rawErrors, state } = props;
|
||||
const { host, workspace, project, repoName } = state;
|
||||
const { host, workspace, project } = state;
|
||||
return (
|
||||
<>
|
||||
{host === 'bitbucket.org' && (
|
||||
@@ -61,19 +61,6 @@ export const BitbucketRepoPicker = (props: {
|
||||
The Project that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !repoName}
|
||||
>
|
||||
<InputLabel htmlFor="repoNameInput">Repository</InputLabel>
|
||||
<Input
|
||||
id="repoNameInput"
|
||||
onChange={e => onChange({ repoName: e.target.value })}
|
||||
value={repoName}
|
||||
/>
|
||||
<FormHelperText>The name of the repository</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,23 +56,4 @@ describe('BitbucketRepoPicker', () => {
|
||||
expect(onChange).toHaveBeenCalledWith({ workspace: 'test-parent' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('repoName field', () => {
|
||||
it('calls onChange when the repoName changes', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<GerritRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'gerrithost.org' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const repoNameInput = getAllByRole('textbox')[2];
|
||||
|
||||
fireEvent.change(repoNameInput, { target: { value: 'test-repo' } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ repoName: 'test-repo' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export const GerritRepoPicker = (props: {
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { onChange, rawErrors, state } = props;
|
||||
const { workspace, repoName, owner } = state;
|
||||
const { workspace, owner } = state;
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
@@ -57,19 +57,6 @@ export const GerritRepoPicker = (props: {
|
||||
The project parent that the repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !repoName}
|
||||
>
|
||||
<InputLabel htmlFor="repoNameInput">Repository</InputLabel>
|
||||
<Input
|
||||
id="repoNameInput"
|
||||
onChange={e => onChange({ repoName: e.target.value })}
|
||||
value={repoName}
|
||||
/>
|
||||
<FormHelperText>The name of the repository</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,24 +84,4 @@ describe('GithubRepoPicker', () => {
|
||||
expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('repo name', () => {
|
||||
it('should render free text field for input of repo name', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const repoNameField = getAllByRole('textbox')[1];
|
||||
fireEvent.change(repoNameField, {
|
||||
target: { value: 'my-mock-repo-name' },
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ repoName: 'my-mock-repo-name' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ export const GithubRepoPicker = (props: {
|
||||
? allowedOwners.map(i => ({ label: i, value: i }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
|
||||
const { owner, repoName } = state;
|
||||
const { owner } = state;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -66,19 +66,6 @@ export const GithubRepoPicker = (props: {
|
||||
The organization, user or project that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !repoName}
|
||||
>
|
||||
<InputLabel htmlFor="repoNameInput">Repository</InputLabel>
|
||||
<Input
|
||||
id="repoNameInput"
|
||||
onChange={e => onChange({ repoName: e.target.value })}
|
||||
value={repoName}
|
||||
/>
|
||||
<FormHelperText>The name of the repository</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,24 +84,4 @@ describe('GitlabRepoPicker', () => {
|
||||
expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('repo name', () => {
|
||||
it('should render free text field for input of repo name', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const repoNameField = getAllByRole('textbox')[1];
|
||||
fireEvent.change(repoNameField, {
|
||||
target: { value: 'my-mock-repo-name' },
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ repoName: 'my-mock-repo-name' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,16 +23,17 @@ import { RepoUrlPickerState } from './types';
|
||||
|
||||
export const GitlabRepoPicker = (props: {
|
||||
allowedOwners?: string[];
|
||||
allowedRepos?: string[];
|
||||
state: RepoUrlPickerState;
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { allowedOwners = [], rawErrors, state, onChange } = props;
|
||||
const { allowedOwners = [], state, onChange, rawErrors } = props;
|
||||
const ownerItems: SelectItem[] = allowedOwners
|
||||
? allowedOwners.map(i => ({ label: i, value: i }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
|
||||
const { owner, repoName } = state;
|
||||
const { owner } = state;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -69,19 +70,6 @@ export const GitlabRepoPicker = (props: {
|
||||
namespaces in gitlab), that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !repoName}
|
||||
>
|
||||
<InputLabel htmlFor="repoNameInput">Repository</InputLabel>
|
||||
<Input
|
||||
id="repoNameInput"
|
||||
onChange={e => onChange({ repoName: e.target.value })}
|
||||
value={repoName}
|
||||
/>
|
||||
<FormHelperText>The name of the repository</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ import { BitbucketRepoPicker } from './BitbucketRepoPicker';
|
||||
import { GerritRepoPicker } from './GerritRepoPicker';
|
||||
import { FieldExtensionComponentProps } from '../../../extensions';
|
||||
import { RepoUrlPickerHost } from './RepoUrlPickerHost';
|
||||
import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName';
|
||||
import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
@@ -40,6 +41,7 @@ import { useTemplateSecrets } from '../../secrets';
|
||||
export interface RepoUrlPickerUiOptions {
|
||||
allowedHosts?: string[];
|
||||
allowedOwners?: string[];
|
||||
allowedRepos?: string[];
|
||||
requestUserCredentials?: {
|
||||
secretsKey: string;
|
||||
additionalScopes?: {
|
||||
@@ -76,6 +78,10 @@ export const RepoUrlPicker = (
|
||||
() => uiSchema?.['ui:options']?.allowedOwners ?? [],
|
||||
[uiSchema],
|
||||
);
|
||||
const allowedRepos = useMemo(
|
||||
() => uiSchema?.['ui:options']?.allowedRepos ?? [],
|
||||
[uiSchema],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(serializeRepoPickerUrl(state));
|
||||
@@ -87,6 +93,11 @@ export const RepoUrlPicker = (
|
||||
setState(prevState => ({ ...prevState, owner: allowedOwners[0] }));
|
||||
}
|
||||
}, [setState, allowedOwners]);
|
||||
useEffect(() => {
|
||||
if (allowedRepos.length === 1) {
|
||||
setState(prevState => ({ ...prevState, repoName: allowedRepos[0] }));
|
||||
}
|
||||
}, [setState, allowedRepos]);
|
||||
|
||||
const updateLocalState = useCallback(
|
||||
(newState: RepoUrlPickerState) => {
|
||||
@@ -179,6 +190,14 @@ export const RepoUrlPicker = (
|
||||
onChange={updateLocalState}
|
||||
/>
|
||||
)}
|
||||
<RepoUrlPickerRepoName
|
||||
repoName={state.repoName}
|
||||
allowedRepos={allowedRepos}
|
||||
onChange={repoName =>
|
||||
setState(prevState => ({ ...prevState, repoName }))
|
||||
}
|
||||
rawErrors={rawErrors}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2022 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 { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
describe('RepoUrlPickerRepoName', () => {
|
||||
it('should call onChange with the first allowed repo if there is none set already', async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
render(
|
||||
<RepoUrlPickerRepoName
|
||||
onChange={onChange}
|
||||
allowedRepos={['foo', 'bar']}
|
||||
rawErrors={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
|
||||
it('should render a dropdown of all the options', async () => {
|
||||
const allowedRepos = ['foo', 'bar'];
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
const { getByRole } = render(
|
||||
<RepoUrlPickerRepoName
|
||||
onChange={onChange}
|
||||
allowedRepos={allowedRepos}
|
||||
rawErrors={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = getByRole('combobox');
|
||||
await fireEvent.click(select);
|
||||
|
||||
for (const option of allowedRepos) {
|
||||
const element = getByRole('option', { name: option });
|
||||
expect(element).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
it('should render a normal text area when no options are passed', async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
const { getByRole } = render(
|
||||
<RepoUrlPickerRepoName
|
||||
onChange={onChange}
|
||||
allowedRepos={[]}
|
||||
rawErrors={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const textArea = getByRole('textbox');
|
||||
|
||||
expect(textArea).toBeVisible();
|
||||
|
||||
fireEvent.change(textArea, { target: { value: 'foo' } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2021 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, { useEffect } from 'react';
|
||||
import { Select, SelectItem } from '@backstage/core-components';
|
||||
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';
|
||||
|
||||
export const RepoUrlPickerRepoName = (props: {
|
||||
repoName?: string;
|
||||
allowedRepos?: string[];
|
||||
onChange: (host: string) => void;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { repoName, allowedRepos, onChange, rawErrors } = props;
|
||||
|
||||
useEffect(() => {
|
||||
// If there is no repoName chosen currently
|
||||
if (!repoName) {
|
||||
// Set the first of the allowedRepos option if that available
|
||||
if (allowedRepos?.length) {
|
||||
onChange(allowedRepos[0]);
|
||||
}
|
||||
}
|
||||
}, [allowedRepos, repoName, onChange]);
|
||||
|
||||
const repoItems: SelectItem[] = allowedRepos
|
||||
? allowedRepos.map(i => ({ label: i, value: i }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !repoName}
|
||||
>
|
||||
{allowedRepos?.length ? (
|
||||
<Select
|
||||
native
|
||||
label="Repositories Available"
|
||||
onChange={selected =>
|
||||
String(Array.isArray(selected) ? selected[0] : selected)
|
||||
}
|
||||
disabled={allowedRepos.length === 1}
|
||||
selected={repoName}
|
||||
items={repoItems}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<InputLabel htmlFor="repoNameInput">Repository</InputLabel>
|
||||
<Input
|
||||
id="repoNameInput"
|
||||
onChange={e => onChange(String(e.target.value))}
|
||||
value={repoName}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FormHelperText>The name of the repository</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user