Merge pull request #8576 from backstage/blam/refactor-repo-url
chore: Refactoring the `RepoUrlPicker`
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Refactoring the `RepoUrlPicker` into separate provider components to encapsulate provider nonsense
|
||||
@@ -36,9 +36,7 @@ export function createScaffolderFieldExtension<T = any>(
|
||||
options: FieldExtensionOptions<T>,
|
||||
): Extension<() => null>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "CustomFieldValidator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type CustomFieldValidator<T> =
|
||||
| ((data: T, field: FieldValidation) => void)
|
||||
| ((
|
||||
@@ -96,9 +94,18 @@ export const EntityTagsPickerFieldExtension: () => null;
|
||||
// @public
|
||||
export const FavouriteTemplate: (props: Props) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "FieldExtensionOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export interface FieldExtensionComponentProps<
|
||||
ReturnValue,
|
||||
UiOptions extends {} = {},
|
||||
> extends FieldProps<ReturnValue> {
|
||||
// (undocumented)
|
||||
uiSchema: {
|
||||
'ui:options'?: UiOptions;
|
||||
};
|
||||
}
|
||||
|
||||
// @public
|
||||
export type FieldExtensionOptions<T = any> = {
|
||||
name: string;
|
||||
component: (props: FieldProps<T>) => JSX.Element | null;
|
||||
@@ -140,18 +147,25 @@ export const OwnerPickerFieldExtension: () => null;
|
||||
// Warning: (ae-missing-release-tag) "RepoUrlPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const RepoUrlPicker: ({
|
||||
onChange,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
}: FieldProps<string>) => JSX.Element;
|
||||
export const RepoUrlPicker: (
|
||||
props: FieldExtensionComponentProps<string, RepoUrlPickerUiOptions>,
|
||||
) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "RepoUrlPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const RepoUrlPickerFieldExtension: () => null;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "RepoUrlPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface RepoUrlPickerUiOptions {
|
||||
// (undocumented)
|
||||
allowedHosts?: string[];
|
||||
// (undocumented)
|
||||
allowedOwners?: string[];
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ScaffolderApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -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 { AzureRepoPicker } from './AzureRepoPicker';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
describe('AzureRepoPicker', () => {
|
||||
it('renders the three input fields', async () => {
|
||||
const { getAllByRole } = render(
|
||||
<AzureRepoPicker onChange={jest.fn()} rawErrors={[]} state={{}} />,
|
||||
);
|
||||
|
||||
const allInputs = getAllByRole('textbox');
|
||||
|
||||
expect(allInputs).toHaveLength(3);
|
||||
});
|
||||
|
||||
describe('org field', () => {
|
||||
it('calls onChange when the organisation changes', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<AzureRepoPicker onChange={onChange} rawErrors={[]} state={{}} />,
|
||||
);
|
||||
|
||||
const orgInput = getAllByRole('textbox')[0];
|
||||
|
||||
fireEvent.change(orgInput, { target: { value: 'org' } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ organization: 'org' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('owner field', () => {
|
||||
it('calls onChange when the owner changes', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<AzureRepoPicker onChange={onChange} rawErrors={[]} state={{}} />,
|
||||
);
|
||||
|
||||
const ownerInput = getAllByRole('textbox')[1];
|
||||
|
||||
fireEvent.change(ownerInput, { target: { value: 'owner' } });
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 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';
|
||||
|
||||
export const AzureRepoPicker = (props: {
|
||||
state: RepoUrlPickerState;
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { rawErrors, state, onChange } = props;
|
||||
const { organization, repoName, owner } = state;
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !organization}
|
||||
>
|
||||
<InputLabel htmlFor="orgInput">Organization</InputLabel>
|
||||
<Input
|
||||
id="orgInput"
|
||||
onChange={e => onChange({ organization: e.target.value })}
|
||||
value={organization}
|
||||
/>
|
||||
<FormHelperText>
|
||||
The organization that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
<InputLabel htmlFor="ownerInput">Owner</InputLabel>
|
||||
<Input
|
||||
id="ownerInput"
|
||||
onChange={e => onChange({ owner: e.target.value })}
|
||||
value={owner}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 { BitbucketRepoPicker } from './BitbucketRepoPicker';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
describe('BitbucketRepoPicker', () => {
|
||||
it('renders workspace input when host is bitbucket.org', () => {
|
||||
const state = { host: 'bitbucket.org', workspace: 'lolsWorkspace' };
|
||||
|
||||
const { getAllByRole } = render(
|
||||
<BitbucketRepoPicker onChange={jest.fn()} rawErrors={[]} state={state} />,
|
||||
);
|
||||
|
||||
expect(getAllByRole('textbox')).toHaveLength(3);
|
||||
expect(getAllByRole('textbox')[0]).toHaveValue('lolsWorkspace');
|
||||
});
|
||||
|
||||
it('hides the workspace input when the host is not bitbucket.org', () => {
|
||||
const state = {
|
||||
host: 'mycustom.domain.bitbucket.org',
|
||||
};
|
||||
|
||||
const { getAllByRole } = render(
|
||||
<BitbucketRepoPicker onChange={jest.fn()} rawErrors={[]} state={state} />,
|
||||
);
|
||||
|
||||
expect(getAllByRole('textbox')).toHaveLength(2);
|
||||
});
|
||||
describe('workspace field', () => {
|
||||
it('calls onChange when the workspace changes', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const workspaceInput = getAllByRole('textbox')[0];
|
||||
|
||||
fireEvent.change(workspaceInput, { target: { value: 'test-workspace' } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ workspace: 'test-workspace' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('project field', () => {
|
||||
it('calls onChange when the project changes', () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<BitbucketRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'bitbucket.org' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const projectInput = getAllByRole('textbox')[1];
|
||||
|
||||
fireEvent.change(projectInput, { target: { value: 'test-project' } });
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
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';
|
||||
|
||||
export const BitbucketRepoPicker = (props: {
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
state: RepoUrlPickerState;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { onChange, rawErrors, state } = props;
|
||||
const { host, workspace, project, repoName } = state;
|
||||
return (
|
||||
<>
|
||||
{host === 'bitbucket.org' && (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !workspace}
|
||||
>
|
||||
<InputLabel htmlFor="workspaceInput">Workspace</InputLabel>
|
||||
<Input
|
||||
id="workspaceInput"
|
||||
onChange={e => onChange({ workspace: e.target.value })}
|
||||
value={workspace}
|
||||
/>
|
||||
<FormHelperText>
|
||||
The Organization that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !project}
|
||||
>
|
||||
<InputLabel htmlFor="projectInput">Project</InputLabel>
|
||||
<Input
|
||||
id="projectInput"
|
||||
onChange={e => onChange({ project: e.target.value })}
|
||||
value={project}
|
||||
/>
|
||||
<FormHelperText>
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 { GithubRepoPicker } from './GithubRepoPicker';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
describe('GithubRepoPicker', () => {
|
||||
describe('owner field', () => {
|
||||
it('renders a select if there is a list of allowed owners', async () => {
|
||||
const allowedOwners = ['owner1', 'owner2'];
|
||||
const { findByText } = render(
|
||||
<GithubRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await findByText('owner1')).toBeInTheDocument();
|
||||
expect(await findByText('owner2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onChange when the owner is changed to a different owner', async () => {
|
||||
const onChange = jest.fn();
|
||||
const allowedOwners = ['owner1', 'owner2'];
|
||||
const { getByRole } = render(
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
);
|
||||
|
||||
await fireEvent.change(getByRole('combobox'), {
|
||||
target: { value: 'owner2' },
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ owner: 'owner2' });
|
||||
});
|
||||
|
||||
it('is disabled picked when only one allowed owner', () => {
|
||||
const onChange = jest.fn();
|
||||
const allowedOwners = ['owner1'];
|
||||
const { getByRole } = render(
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByRole('combobox')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should display free text if no allowed owners are passed', async () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
/>,
|
||||
);
|
||||
const ownerField = getAllByRole('textbox')[0];
|
||||
fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } });
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
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 { Select, SelectItem } from '@backstage/core-components';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
|
||||
export const GithubRepoPicker = (props: {
|
||||
allowedOwners?: string[];
|
||||
rawErrors: string[];
|
||||
state: RepoUrlPickerState;
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
}) => {
|
||||
const { allowedOwners = [], rawErrors, state, onChange } = props;
|
||||
const ownerItems: SelectItem[] = allowedOwners
|
||||
? allowedOwners.map(i => ({ label: i, value: i }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
|
||||
const { owner, repoName } = state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
{allowedOwners?.length ? (
|
||||
<Select
|
||||
native
|
||||
label="Owner Available"
|
||||
onChange={s =>
|
||||
onChange({ owner: String(Array.isArray(s) ? s[0] : s) })
|
||||
}
|
||||
disabled={allowedOwners.length === 1}
|
||||
selected={owner}
|
||||
items={ownerItems}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<InputLabel htmlFor="ownerInput">Owner</InputLabel>
|
||||
<Input
|
||||
id="ownerInput"
|
||||
onChange={e => onChange({ owner: e.target.value })}
|
||||
value={owner}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FormHelperText>
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 { GitlabRepoPicker } from './GitlabRepoPicker';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
describe('GitlabRepoPicker', () => {
|
||||
describe('owner field', () => {
|
||||
it('renders a select if there is a list of allowed owners', async () => {
|
||||
const allowedOwners = ['owner1', 'owner2'];
|
||||
const { findByText } = render(
|
||||
<GitlabRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await findByText('owner1')).toBeInTheDocument();
|
||||
expect(await findByText('owner2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onChange when the owner is changed to a different owner', async () => {
|
||||
const onChange = jest.fn();
|
||||
const allowedOwners = ['owner1', 'owner2'];
|
||||
const { getByRole } = render(
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
);
|
||||
|
||||
await fireEvent.change(getByRole('combobox'), {
|
||||
target: { value: 'owner2' },
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ owner: 'owner2' });
|
||||
});
|
||||
|
||||
it('is disabled picked when only one allowed owner', () => {
|
||||
const onChange = jest.fn();
|
||||
const allowedOwners = ['owner1'];
|
||||
const { getByRole } = render(
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByRole('combobox')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should display free text if no allowed owners are passed', async () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = render(
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
/>,
|
||||
);
|
||||
const ownerField = getAllByRole('textbox')[0];
|
||||
fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } });
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
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 { Select, SelectItem } from '@backstage/core-components';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
|
||||
export const GitlabRepoPicker = (props: {
|
||||
allowedOwners?: string[];
|
||||
state: RepoUrlPickerState;
|
||||
onChange: (state: RepoUrlPickerState) => void;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { allowedOwners = [], rawErrors, state, onChange } = props;
|
||||
const ownerItems: SelectItem[] = allowedOwners
|
||||
? allowedOwners.map(i => ({ label: i, value: i }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
|
||||
const { owner, repoName } = state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
{allowedOwners?.length ? (
|
||||
<Select
|
||||
native
|
||||
label="Owner Available"
|
||||
onChange={selected =>
|
||||
onChange({
|
||||
owner: String(Array.isArray(selected) ? selected[0] : selected),
|
||||
})
|
||||
}
|
||||
disabled={allowedOwners.length === 1}
|
||||
selected={owner}
|
||||
items={ownerItems}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<InputLabel htmlFor="ownerInput">Owner</InputLabel>
|
||||
<Input
|
||||
id="ownerInput"
|
||||
onChange={e => onChange({ owner: e.target.value })}
|
||||
value={owner}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FormHelperText>
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -13,371 +13,100 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
Progress,
|
||||
Select,
|
||||
SelectedItems,
|
||||
SelectItem,
|
||||
} from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
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 { FieldProps } from '@rjsf/core';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { scaffolderApiRef } from '../../../api';
|
||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { GithubRepoPicker } from './GithubRepoPicker';
|
||||
import { GitlabRepoPicker } from './GitlabRepoPicker';
|
||||
import { AzureRepoPicker } from './AzureRepoPicker';
|
||||
import { BitbucketRepoPicker } from './BitbucketRepoPicker';
|
||||
import { FieldExtensionComponentProps } from '../../../extensions';
|
||||
import { RepoUrlPickerHost } from './RepoUrlPickerHost';
|
||||
import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils';
|
||||
import { RepoUrlPickerState } from './types';
|
||||
|
||||
function splitFormData(url: string | undefined, allowedOwners?: string[]) {
|
||||
let host = undefined;
|
||||
let owner = undefined;
|
||||
let repo = undefined;
|
||||
let organization = undefined;
|
||||
let workspace = undefined;
|
||||
let project = undefined;
|
||||
|
||||
try {
|
||||
if (url) {
|
||||
const parsed = new URL(`https://${url}`);
|
||||
host = parsed.host;
|
||||
owner = parsed.searchParams.get('owner') || allowedOwners?.[0];
|
||||
repo = parsed.searchParams.get('repo') || undefined;
|
||||
// This is azure dev ops specific. not used for any other provider.
|
||||
organization = parsed.searchParams.get('organization') || undefined;
|
||||
// These are bitbucket specific, not used for any other provider.
|
||||
workspace = parsed.searchParams.get('workspace') || undefined;
|
||||
project = parsed.searchParams.get('project') || undefined;
|
||||
}
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
|
||||
return { host, owner, repo, organization, workspace, project };
|
||||
export interface RepoUrlPickerUiOptions {
|
||||
allowedHosts?: string[];
|
||||
allowedOwners?: string[];
|
||||
}
|
||||
|
||||
function serializeFormData(data: {
|
||||
host?: string;
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
organization?: string;
|
||||
workspace?: string;
|
||||
project?: string;
|
||||
}) {
|
||||
if (!data.host) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (data.owner) {
|
||||
params.set('owner', data.owner);
|
||||
}
|
||||
if (data.repo) {
|
||||
params.set('repo', data.repo);
|
||||
}
|
||||
if (data.organization) {
|
||||
params.set('organization', data.organization);
|
||||
}
|
||||
if (data.workspace) {
|
||||
params.set('workspace', data.workspace);
|
||||
}
|
||||
if (data.project) {
|
||||
params.set('project', data.project);
|
||||
}
|
||||
|
||||
return `${data.host}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export const RepoUrlPicker = ({
|
||||
onChange,
|
||||
uiSchema,
|
||||
rawErrors,
|
||||
formData,
|
||||
}: FieldProps<string>) => {
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
export const RepoUrlPicker = (
|
||||
props: FieldExtensionComponentProps<string, RepoUrlPickerUiOptions>,
|
||||
) => {
|
||||
const { uiSchema, onChange, rawErrors, formData } = props;
|
||||
const [state, setState] = useState<RepoUrlPickerState>(
|
||||
parseRepoPickerUrl(formData),
|
||||
);
|
||||
const integrationApi = useApi(scmIntegrationsApiRef);
|
||||
const allowedHosts = uiSchema['ui:options']?.allowedHosts as string[];
|
||||
const allowedOwners = uiSchema['ui:options']?.allowedOwners as string[];
|
||||
|
||||
const { value: integrations, loading } = useAsync(async () => {
|
||||
return await scaffolderApi.getIntegrationsList({ allowedHosts });
|
||||
});
|
||||
|
||||
const { host, owner, repo, organization, workspace, project } = splitFormData(
|
||||
formData,
|
||||
allowedOwners,
|
||||
const allowedHosts = useMemo(
|
||||
() => uiSchema?.['ui:options']?.allowedHosts ?? [],
|
||||
[uiSchema],
|
||||
);
|
||||
const updateHost = useCallback(
|
||||
(value: SelectedItems) => {
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host: value as string,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[onChange, owner, repo, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateOwnerSelect = useCallback(
|
||||
(value: SelectedItems) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
owner: value as string,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, repo, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateOwner = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
owner: evt.target.value as string,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, repo, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateRepo = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
owner,
|
||||
repo: evt.target.value as string,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateOrganization = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
organization: evt.target.value as string,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, repo, workspace, project],
|
||||
);
|
||||
|
||||
const updateWorkspace = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace: evt.target.value as string,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, repo, organization, project],
|
||||
);
|
||||
|
||||
const updateProject = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project: evt.target.value as string,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, repo, organization, workspace],
|
||||
const allowedOwners = useMemo(
|
||||
() => uiSchema?.['ui:options']?.allowedOwners ?? [],
|
||||
[uiSchema],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (host === undefined && integrations?.length) {
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host: integrations[0].host,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
);
|
||||
onChange(serializeRepoPickerUrl(state));
|
||||
}, [state, onChange]);
|
||||
|
||||
/* we deal with calling the repo setting here instead of in each components for ease */
|
||||
useEffect(() => {
|
||||
if (allowedOwners.length === 1) {
|
||||
setState(prevState => ({ ...prevState, owner: allowedOwners[0] }));
|
||||
}
|
||||
}, [
|
||||
onChange,
|
||||
integrations,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
]);
|
||||
}, [setState, allowedOwners]);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
const updateLocalState = useCallback(
|
||||
(newState: RepoUrlPickerState) => {
|
||||
setState(prevState => ({ ...prevState, ...newState }));
|
||||
},
|
||||
[setState],
|
||||
);
|
||||
|
||||
const hostsOptions: SelectItem[] = integrations
|
||||
? integrations
|
||||
.filter(i => allowedHosts?.includes(i.host))
|
||||
.map(i => ({ label: i.title, value: i.host }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
|
||||
const ownersOptions: SelectItem[] = allowedOwners
|
||||
? allowedOwners.map(i => ({ label: i, value: i }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
const hostType =
|
||||
(state.host && integrationApi.byHost(state.host)?.type) ?? null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !host}
|
||||
>
|
||||
<Select
|
||||
native
|
||||
disabled={hostsOptions.length === 1}
|
||||
label="Host"
|
||||
onChange={updateHost}
|
||||
selected={host}
|
||||
items={hostsOptions}
|
||||
<RepoUrlPickerHost
|
||||
host={state.host}
|
||||
hosts={allowedHosts}
|
||||
onChange={host => setState(prevState => ({ ...prevState, host }))}
|
||||
rawErrors={rawErrors}
|
||||
/>
|
||||
{hostType === 'github' && (
|
||||
<GithubRepoPicker
|
||||
allowedOwners={allowedOwners}
|
||||
rawErrors={rawErrors}
|
||||
state={state}
|
||||
onChange={updateLocalState}
|
||||
/>
|
||||
|
||||
<FormHelperText>
|
||||
The host where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
{/* Show this for dev.azure.com only */}
|
||||
{host === 'dev.azure.com' && (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !organization}
|
||||
>
|
||||
<InputLabel htmlFor="repoInput">Organization</InputLabel>
|
||||
<Input
|
||||
id="repoInput"
|
||||
onChange={updateOrganization}
|
||||
value={organization}
|
||||
/>
|
||||
<FormHelperText>The name of the organization</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
{host && integrationApi.byHost(host)?.type === 'bitbucket' && (
|
||||
<>
|
||||
{/* Show this for bitbucket.org only */}
|
||||
{host === 'bitbucket.org' && (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !workspace}
|
||||
>
|
||||
<InputLabel htmlFor="wokrspaceInput">Workspace</InputLabel>
|
||||
<Input
|
||||
id="wokrspaceInput"
|
||||
onChange={updateWorkspace}
|
||||
value={workspace}
|
||||
/>
|
||||
<FormHelperText>
|
||||
The workspace where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !project}
|
||||
>
|
||||
<InputLabel htmlFor="wokrspaceInput">Project</InputLabel>
|
||||
<Input
|
||||
id="wokrspaceInput"
|
||||
onChange={updateProject}
|
||||
value={project}
|
||||
/>
|
||||
<FormHelperText>
|
||||
The project where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
{hostType === 'gitlab' && (
|
||||
<GitlabRepoPicker
|
||||
allowedOwners={allowedOwners}
|
||||
rawErrors={rawErrors}
|
||||
state={state}
|
||||
onChange={updateLocalState}
|
||||
/>
|
||||
)}
|
||||
{hostType === 'bitbucket' && (
|
||||
<BitbucketRepoPicker
|
||||
rawErrors={rawErrors}
|
||||
state={state}
|
||||
onChange={updateLocalState}
|
||||
/>
|
||||
)}
|
||||
{hostType === 'azure' && (
|
||||
<AzureRepoPicker
|
||||
rawErrors={rawErrors}
|
||||
state={state}
|
||||
onChange={updateLocalState}
|
||||
/>
|
||||
)}
|
||||
{/* Show this for all hosts except bitbucket */}
|
||||
{host &&
|
||||
integrationApi.byHost(host)?.type !== 'bitbucket' &&
|
||||
!allowedOwners && (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
<InputLabel htmlFor="ownerInput">Owner</InputLabel>
|
||||
<Input id="ownerInput" onChange={updateOwner} value={owner} />
|
||||
<FormHelperText>
|
||||
The organization, user or project that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
{/* Show this for all hosts except bitbucket where allowed owner is set */}
|
||||
{host &&
|
||||
integrationApi.byHost(host)?.type !== 'bitbucket' &&
|
||||
allowedOwners && (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
<Select
|
||||
native
|
||||
label="Owner Available"
|
||||
onChange={updateOwnerSelect}
|
||||
disabled={ownersOptions.length === 1}
|
||||
selected={owner}
|
||||
items={ownersOptions}
|
||||
/>
|
||||
|
||||
<FormHelperText>
|
||||
The organization, user or project that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
{/* Show this for all hosts */}
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !repo}
|
||||
>
|
||||
<InputLabel htmlFor="repoInput">Repository</InputLabel>
|
||||
<Input id="repoInput" onChange={updateRepo} value={repo} />
|
||||
<FormHelperText>The name of the repository</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
import { RepoUrlPickerHost } from './RepoUrlPickerHost';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { scaffolderApiRef } from '../../../api';
|
||||
import { fireEvent, within } from '@testing-library/react';
|
||||
|
||||
describe('RepoUrlPickerHostField', () => {
|
||||
it('renders the default host properly', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const mockScaffolderApi = {
|
||||
getIntegrationsList: jest
|
||||
.fn()
|
||||
.mockResolvedValue([
|
||||
{ host: 'github.com', title: 'github.com', type: 'github' },
|
||||
]),
|
||||
};
|
||||
const { getByText } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
|
||||
<RepoUrlPickerHost
|
||||
hosts={['github.com']}
|
||||
onChange={mockOnChange}
|
||||
rawErrors={[]}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(getByText('github.com')).toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledWith('github.com');
|
||||
});
|
||||
|
||||
it('should provide a dropdown when multiple hosts are returned that can be selected', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const mockScaffolderApi = {
|
||||
getIntegrationsList: jest.fn().mockResolvedValue([
|
||||
{ host: 'github.com', title: 'github.com', type: 'github' },
|
||||
{ host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' },
|
||||
]),
|
||||
};
|
||||
|
||||
const { getByRole, getByText, getByTestId } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
|
||||
<RepoUrlPickerHost
|
||||
hosts={['github.com', 'gitlab.com']}
|
||||
onChange={mockOnChange}
|
||||
rawErrors={[]}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
fireEvent.mouseDown(getByTestId('select'));
|
||||
expect(getByText('gitlab.com')).toBeInTheDocument();
|
||||
|
||||
const listbox = within(getByRole('combobox'));
|
||||
|
||||
expect(listbox.getAllByRole('option')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not display hosts that dont have integration config set correctly', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const mockScaffolderApi = {
|
||||
getIntegrationsList: jest.fn().mockResolvedValue([
|
||||
{ host: 'github.com', title: 'github.com', type: 'github' },
|
||||
{ host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' },
|
||||
]),
|
||||
};
|
||||
|
||||
const { getByRole, getByText, getByTestId } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
|
||||
<RepoUrlPickerHost
|
||||
hosts={['github.com', 'gitlab.com', 'notfound.host']}
|
||||
onChange={mockOnChange}
|
||||
rawErrors={[]}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
fireEvent.mouseDown(getByTestId('select'));
|
||||
expect(getByText('gitlab.com')).toBeInTheDocument();
|
||||
|
||||
const listbox = within(getByRole('combobox'));
|
||||
|
||||
expect(listbox.getAllByRole('option')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 { Progress, Select, SelectItem } from '@backstage/core-components';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { scaffolderApiRef } from '../../../api';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
export const RepoUrlPickerHost = (props: {
|
||||
host?: string;
|
||||
hosts?: string[];
|
||||
onChange: (host: string) => void;
|
||||
rawErrors: string[];
|
||||
}) => {
|
||||
const { host, hosts, onChange, rawErrors } = props;
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
|
||||
const { value: integrations, loading } = useAsync(async () => {
|
||||
return await scaffolderApi.getIntegrationsList({
|
||||
allowedHosts: hosts ?? [],
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hosts && !host) {
|
||||
// This is only hear to set the default as the first one in the hosts array
|
||||
// if the host is not set yet and there is a list of hosts.
|
||||
onChange(hosts[0]);
|
||||
}
|
||||
}, [hosts, host, onChange]);
|
||||
|
||||
const hostsOptions: SelectItem[] = integrations
|
||||
? integrations
|
||||
.filter(i => hosts?.includes(i.host))
|
||||
.map(i => ({ label: i.title, value: i.host }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !host}
|
||||
>
|
||||
<Select
|
||||
native
|
||||
disabled={hosts?.length === 1 ?? false}
|
||||
label="Host"
|
||||
onChange={s => onChange(String(Array.isArray(s) ? s[0] : s))}
|
||||
selected={host}
|
||||
items={hostsOptions}
|
||||
data-testid="host-select"
|
||||
/>
|
||||
|
||||
<FormHelperText>
|
||||
The host where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -14,4 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { RepoUrlPicker } from './RepoUrlPicker';
|
||||
export type { RepoUrlPickerUiOptions } from './RepoUrlPicker';
|
||||
export { repoPickerValidation } from './validation';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export interface RepoUrlPickerState {
|
||||
host?: string;
|
||||
owner?: string;
|
||||
repoName?: string;
|
||||
organization?: string;
|
||||
workspace?: string;
|
||||
project?: string;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('serializeRepoPickerUrl', () => {
|
||||
it('should return undefined when host is not set', () => {
|
||||
expect(serializeRepoPickerUrl({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set the correct owner and repo', () => {
|
||||
expect(
|
||||
serializeRepoPickerUrl({
|
||||
host: 'github.com',
|
||||
owner: 'owner',
|
||||
repoName: 'backstage',
|
||||
}),
|
||||
).toBe('github.com?owner=owner&repo=backstage');
|
||||
});
|
||||
|
||||
it('should set correct other options', () => {
|
||||
expect(
|
||||
serializeRepoPickerUrl({
|
||||
host: 'github.com',
|
||||
organization: 'organization',
|
||||
workspace: 'workspace',
|
||||
project: 'backstage',
|
||||
}),
|
||||
).toBe(
|
||||
'github.com?organization=organization&workspace=workspace&project=backstage',
|
||||
);
|
||||
});
|
||||
|
||||
it('should set all correct options', () => {
|
||||
expect(
|
||||
serializeRepoPickerUrl({
|
||||
host: 'github.com',
|
||||
owner: 'owner',
|
||||
repoName: 'backstage',
|
||||
organization: 'organization',
|
||||
workspace: 'workspace',
|
||||
project: 'backstage',
|
||||
}),
|
||||
).toBe(
|
||||
'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseRepoPickerUrl', () => {
|
||||
it('should parse a complete string', () => {
|
||||
expect(
|
||||
parseRepoPickerUrl(
|
||||
'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage',
|
||||
),
|
||||
).toEqual({
|
||||
host: 'github.com',
|
||||
owner: 'owner',
|
||||
repoName: 'backstage',
|
||||
organization: 'organization',
|
||||
workspace: 'workspace',
|
||||
project: 'backstage',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 { RepoUrlPickerState } from './types';
|
||||
|
||||
export function serializeRepoPickerUrl(data: RepoUrlPickerState) {
|
||||
if (!data.host) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (data.owner) {
|
||||
params.set('owner', data.owner);
|
||||
}
|
||||
if (data.repoName) {
|
||||
params.set('repo', data.repoName);
|
||||
}
|
||||
if (data.organization) {
|
||||
params.set('organization', data.organization);
|
||||
}
|
||||
if (data.workspace) {
|
||||
params.set('workspace', data.workspace);
|
||||
}
|
||||
if (data.project) {
|
||||
params.set('project', data.project);
|
||||
}
|
||||
|
||||
return `${data.host}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function parseRepoPickerUrl(
|
||||
url: string | undefined,
|
||||
): RepoUrlPickerState {
|
||||
let host = undefined;
|
||||
let owner = undefined;
|
||||
let repoName = undefined;
|
||||
let organization = undefined;
|
||||
let workspace = undefined;
|
||||
let project = undefined;
|
||||
|
||||
try {
|
||||
if (url) {
|
||||
const parsed = new URL(`https://${url}`);
|
||||
host = parsed.host;
|
||||
owner = parsed.searchParams.get('owner') || undefined;
|
||||
repoName = parsed.searchParams.get('repo') || undefined;
|
||||
organization = parsed.searchParams.get('organization') || undefined;
|
||||
workspace = parsed.searchParams.get('workspace') || undefined;
|
||||
project = parsed.searchParams.get('project') || undefined;
|
||||
}
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
|
||||
return { host, owner, repoName, organization, workspace, project };
|
||||
}
|
||||
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CustomFieldValidator, FieldExtensionOptions } from './types';
|
||||
import {
|
||||
CustomFieldValidator,
|
||||
FieldExtensionOptions,
|
||||
FieldExtensionComponentProps,
|
||||
} from './types';
|
||||
import { Extension, attachComponentData } from '@backstage/core-plugin-api';
|
||||
|
||||
export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1';
|
||||
@@ -46,6 +50,10 @@ attachComponentData(
|
||||
true,
|
||||
);
|
||||
|
||||
export type { CustomFieldValidator, FieldExtensionOptions };
|
||||
export type {
|
||||
CustomFieldValidator,
|
||||
FieldExtensionOptions,
|
||||
FieldExtensionComponentProps,
|
||||
};
|
||||
|
||||
export { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from './default';
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { FieldValidation, FieldProps } from '@rjsf/core';
|
||||
|
||||
/**
|
||||
* Field validation type for Custom Field Extensions.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type CustomFieldValidator<T> =
|
||||
| ((data: T, field: FieldValidation) => void)
|
||||
| ((
|
||||
@@ -24,8 +29,29 @@ export type CustomFieldValidator<T> =
|
||||
context: { apiHolder: ApiHolder },
|
||||
) => void);
|
||||
|
||||
/**
|
||||
* Type for the Custom Field Extension with the
|
||||
* name and components and validation function.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type FieldExtensionOptions<T = any> = {
|
||||
name: string;
|
||||
component: (props: FieldProps<T>) => JSX.Element | null;
|
||||
validation?: CustomFieldValidator<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type for field extensions and being able to type
|
||||
* incoming props easier.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface FieldExtensionComponentProps<
|
||||
ReturnValue,
|
||||
UiOptions extends {} = {},
|
||||
> extends FieldProps<ReturnValue> {
|
||||
uiSchema: {
|
||||
'ui:options'?: UiOptions;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,11 @@ export {
|
||||
createScaffolderFieldExtension,
|
||||
ScaffolderFieldExtensions,
|
||||
} from './extensions';
|
||||
export type { CustomFieldValidator, FieldExtensionOptions } from './extensions';
|
||||
export type {
|
||||
CustomFieldValidator,
|
||||
FieldExtensionOptions,
|
||||
FieldExtensionComponentProps,
|
||||
} from './extensions';
|
||||
export {
|
||||
EntityPickerFieldExtension,
|
||||
EntityNamePickerFieldExtension,
|
||||
@@ -47,6 +51,7 @@ export {
|
||||
TextValuePicker,
|
||||
OwnedEntityPicker,
|
||||
} from './components/fields';
|
||||
export type { RepoUrlPickerUiOptions } from './components/fields';
|
||||
export { FavouriteTemplate } from './components/FavouriteTemplate';
|
||||
export { TemplateList } from './components/TemplateList';
|
||||
export type { TemplateListProps } from './components/TemplateList';
|
||||
|
||||
Reference in New Issue
Block a user