From 02274caa49cb71a5f1922bed4e0cb4576ba8490c Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 22 Dec 2021 01:25:33 +0100 Subject: [PATCH 01/22] chore: moving out the host field into another component Signed-off-by: blam Signed-off-by: blam --- .../components/fields/RepoUrlPicker/host.tsx | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/host.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/host.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/host.tsx new file mode 100644 index 0000000000..4d841d8665 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/host.tsx @@ -0,0 +1,73 @@ +/* + * 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 { + Progress, + Select, + SelectedItems, + 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 RepoUrlPickerHost = ({ + host, + organization, + hosts, + onChange, + rawErrors, +}) => { + return ( + <> + 0 && !host} + > + + The name of the organization + + )} + + ); +}; From fece75f0387faaa5ab668b1f2f9fd6d8a24982b5 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 22 Dec 2021 02:10:32 +0100 Subject: [PATCH 02/22] chore Signed-off-by: blam --- .../src/components/fields/RepoUrlPicker/{host.tsx => gbost.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plugins/scaffolder/src/components/fields/RepoUrlPicker/{host.tsx => gbost.tsx} (100%) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/host.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/gbost.tsx similarity index 100% rename from plugins/scaffolder/src/components/fields/RepoUrlPicker/host.tsx rename to plugins/scaffolder/src/components/fields/RepoUrlPicker/gbost.tsx From 0ca7eacd648eac174d54f58d9a7bc28883001bd2 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 22 Dec 2021 02:10:53 +0100 Subject: [PATCH 03/22] chore: added Host widget Signed-off-by: blam Signed-off-by: blam --- .../src/components/fields/RepoUrlPicker/{gbost.tsx => Host.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plugins/scaffolder/src/components/fields/RepoUrlPicker/{gbost.tsx => Host.tsx} (100%) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/gbost.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/Host.tsx similarity index 100% rename from plugins/scaffolder/src/components/fields/RepoUrlPicker/gbost.tsx rename to plugins/scaffolder/src/components/fields/RepoUrlPicker/Host.tsx From 49299ac371f4400ac42bfc83ec4874609f45f06a Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 22 Dec 2021 04:28:45 +0100 Subject: [PATCH 04/22] chore: starting the refactor and split of the RepoUrlPicker Signed-off-by: blam --- .../fields/RepoUrlPicker/AzureRepoPicker.tsx | 84 +++++++++ .../RepoUrlPicker/BitbucketRepoPicker.tsx | 80 +++++++++ .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 87 ++++++++++ .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 87 ++++++++++ .../fields/RepoUrlPicker/NewRepoUrlPicker.tsx | 160 ++++++++++++++++++ .../{Host.tsx => RepoUrlPickerHost.tsx} | 59 ++++--- .../components/fields/RepoUrlPicker/index.ts | 2 +- plugins/scaffolder/src/extensions/index.tsx | 12 +- plugins/scaffolder/src/extensions/types.ts | 7 + 9 files changed, 552 insertions(+), 26 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/NewRepoUrlPicker.tsx rename plugins/scaffolder/src/components/fields/RepoUrlPicker/{Host.tsx => RepoUrlPickerHost.tsx} (55%) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx new file mode 100644 index 0000000000..df6b105368 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx @@ -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'; + +export const AzureRepoPicker = ({ + onOrgChange, + onOwnerChange, + onRepoNameChange, + rawErrors, + org, + owner, + repoName, +}: { + onOrgChange: (org: string) => void; + onOwnerChange: (owner: string) => void; + onRepoNameChange: (name: string) => void; + owner?: string; + org?: string; + repoName?: string; + rawErrors: string[]; +}) => { + return ( + <> + 0 && !org} + > + Organization + onOrgChange(e.target.value)} + value={org} + /> + + The organization that this repo will belong to + + + 0 && !owner} + > + Owner + onOwnerChange(e.target.value)} + value={owner} + /> + The Owner that this repo will belong to + + 0 && !repoName} + > + Repository + onRepoNameChange(e.target.value)} + value={repoName} + /> + The name of the repository + + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx new file mode 100644 index 0000000000..40828e5852 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -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 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'; + +export const BitbucketRepoPicker = ({ + onProjectChange, + onWorkspaceChange, + onRepoNameChange, + rawErrors, + workspace, + project, + host, + repoName, +}: { + onProjectChange: (owner: string) => void; + onWorkspaceChange: (name: string) => void; + onRepoNameChange: (name: string) => void; + workspace?: string; + project?: string; + repoName?: string; + host: string; + rawErrors: string[]; +}) => { + return ( + <> + 0 && !project && !workspace} + > + + {host === 'bitbucket.org' ? 'Workspace' : 'Project'} + + { + return host === 'bitbucket.org' + ? onWorkspaceChange(e.target.value) + : onProjectChange(e.target.value); + }} + value={host === 'bitbucket.org' ? workspace : project} + /> + + The {host === 'bitbucket.org' ? 'Workspace' : 'Project'}that this repo + will belong to + + + 0 && !repoName} + > + Repository + onRepoNameChange(e.target.value)} + value={repoName} + /> + The name of the repository + + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx new file mode 100644 index 0000000000..53126420f3 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -0,0 +1,87 @@ +/* + * 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'; + +export const GithubRepoPicker = ({ + onOwnerChange, + onRepoNameChange, + allowedOwners = [], + rawErrors, + owner, + repoName, +}: { + onOwnerChange: (owner: string) => void; + onRepoNameChange: (name: string) => void; + allowedOwners: string[]; + owner?: string; + repoName?: string; + rawErrors: string[]; +}) => { + const ownerItems: SelectItem[] = allowedOwners + ? allowedOwners.map(i => ({ label: i, value: i })) + : [{ label: 'Loading...', value: 'loading' }]; + + return ( + <> + 0 && !owner} + > + {allowedOwners?.length ? ( + onOwnerChange(e.target.value)} + value={owner} + /> + + )} + + The organization, user or project that this repo will belong to + + + 0 && !repoName} + > + Repository + onRepoNameChange(e.target.value)} + value={repoName} + /> + The name of the repository + + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx new file mode 100644 index 0000000000..2aecc76fbc --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -0,0 +1,87 @@ +/* + * 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'; + +export const GitlabRepoPicker = ({ + onOwnerChange, + onRepoNameChange, + allowedOwners = [], + rawErrors, + owner, + repoName, +}: { + onOwnerChange: (owner: string) => void; + onRepoNameChange: (name: string) => void; + allowedOwners: string[]; + owner?: string; + repoName?: string; + rawErrors: string[]; +}) => { + const ownerItems: SelectItem[] = allowedOwners + ? allowedOwners.map(i => ({ label: i, value: i })) + : [{ label: 'Loading...', value: 'loading' }]; + + return ( + <> + 0 && !owner} + > + {allowedOwners?.length ? ( + onOwnerChange(e.target.value)} + value={owner} + /> + + )} + + The organization, user or project that this repo will belong to + + + 0 && !repoName} + > + Repository + onRepoNameChange(e.target.value)} + value={repoName} + /> + The name of the repository + + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/NewRepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/NewRepoUrlPicker.tsx new file mode 100644 index 0000000000..50a8101165 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/NewRepoUrlPicker.tsx @@ -0,0 +1,160 @@ +/* + * 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 { useApi } from '@backstage/core-plugin-api'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import React, { useEffect, useState } from 'react'; +import { GithubRepoPicker } from './GithubRepoPicker'; +import { GitlabRepoPicker } from './GitlabRepoPicker'; +import { AzureRepoPicker } from './AzureRepoPicker'; +import { BitbucketRepoPicker } from './BitbucketRepoPicker'; +import { CustomFieldExtension } from '../../../extensions'; +import { RepoUrlPickerHost } from './RepoUrlPickerHost'; + +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 = ({ + uiSchema, + onChange, + rawErrors, + formData, +}: CustomFieldExtension) => { + const [state, setState] = useState<{ + host?: string; + owner?: string; + repo?: string; + organization?: string; + workspace?: string; + project?: string; + }>({}); + const integrationApi = useApi(scmIntegrationsApiRef); + + const allowedHosts = uiSchema?.['ui:options']?.allowedHosts ?? []; + const allowedOwners = uiSchema?.['ui:options']?.allowedOwners ?? []; + + useEffect(() => { + onChange(serializeFormData(state)); + }, [state, onChange]); + + return ( + <> + setState({ host })} + rawErrors={rawErrors} + /> + {state.host && integrationApi.byHost(state.host)?.type === 'github' && ( + + setState(prevState => ({ ...prevState, repo })) + } + onOwnerChange={owner => + setState(prevState => ({ ...prevState, owner })) + } + /> + )} + {state.host && integrationApi.byHost(state.host)?.type === 'gitlab' && ( + + setState(prevState => ({ ...prevState, repo })) + } + onOwnerChange={owner => + setState(prevState => ({ ...prevState, owner })) + } + /> + )} + {state.host && + integrationApi.byHost(state.host)?.type === 'bitbucket' && ( + + setState(prevState => ({ ...prevState, repo })) + } + onProjectChange={project => + setState(prevState => ({ ...prevState, project })) + } + onWorkspaceChange={workspace => + setState(prevState => ({ ...prevState, workspace })) + } + /> + )} + {state.host && integrationApi.byHost(state.host)?.type === 'azure' && ( + + setState(prevState => ({ ...prevState, owner })) + } + onRepoNameChange={repo => + setState(prevState => ({ ...prevState, repo })) + } + onOrgChange={org => + setState(prevState => ({ ...prevState, organization: org })) + } + /> + )} + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/Host.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx similarity index 55% rename from plugins/scaffolder/src/components/fields/RepoUrlPicker/Host.tsx rename to plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx index 4d841d8665..8e270610bc 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/Host.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { Progress, Select, @@ -22,16 +22,45 @@ import { } 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'; +import { useApi } from '@backstage/core-plugin-api'; +import { scaffolderApiRef } from '../../../api'; +import { useAsync } from 'react-use'; export const RepoUrlPickerHost = ({ host, - organization, hosts, onChange, rawErrors, +}: { + host?: string; + hosts?: string[]; + onChange: (host: string) => void; + rawErrors: string[]; }) => { + const scaffolderApi = useApi(scaffolderApiRef); + + const { value: integrations, loading } = useAsync(async () => { + return await scaffolderApi.getIntegrationsList({ + allowedHosts: hosts ?? [], + }); + }); + + useEffect(() => { + if (hosts && !host) { + 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 ; + } + return ( <> - The name of the organization - - )} ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index 7e14d6d62b..99bcff332b 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { RepoUrlPicker } from './RepoUrlPicker'; +export { RepoUrlPicker } from './NewRepoUrlPicker'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 131cd105da..9d5cf4f0b4 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -15,7 +15,11 @@ */ import React from 'react'; -import { CustomFieldValidator, FieldExtensionOptions } from './types'; +import { + CustomFieldValidator, + FieldExtensionOptions, + CustomFieldExtension, +} 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, + CustomFieldExtension, +}; export { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from './default'; diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 644897f7f4..12339f5be7 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -29,3 +29,10 @@ export type FieldExtensionOptions = { component: (props: FieldProps) => JSX.Element | null; validation?: CustomFieldValidator; }; + +export interface CustomFieldExtension + extends FieldProps { + uiSchema: { + 'ui:options'?: UiOptions; + }; +} From 2632ae4c2f2487986b82c2dad3280e6483574886 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 22 Dec 2021 04:36:58 +0100 Subject: [PATCH 05/22] chore: fixing bitbucket behaviour for cloud and noncloud things Signed-off-by: blam --- .../RepoUrlPicker/BitbucketRepoPicker.tsx | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 40828e5852..7712a490b2 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -40,26 +40,36 @@ export const BitbucketRepoPicker = ({ }) => { return ( <> + {host === 'bitbucket.org' && ( + 0 && !workspace} + > + Workspace + onWorkspaceChange(e.target.value)} + value={workspace} + /> + + The Organization that this repo will belong to + + + )} 0 && !project && !workspace} + error={rawErrors?.length > 0 && !project} > - - {host === 'bitbucket.org' ? 'Workspace' : 'Project'} - + Project { - return host === 'bitbucket.org' - ? onWorkspaceChange(e.target.value) - : onProjectChange(e.target.value); - }} - value={host === 'bitbucket.org' ? workspace : project} + id="projectInput" + onChange={e => onProjectChange(e.target.value)} + value={project} /> - The {host === 'bitbucket.org' ? 'Workspace' : 'Project'}that this repo - will belong to + The Project that this repo will belong to Date: Wed, 22 Dec 2021 04:50:54 +0100 Subject: [PATCH 06/22] chore: think i've finished the refactor of this bloody component now Signed-off-by: blam --- .../fields/RepoUrlPicker/NewRepoUrlPicker.tsx | 160 ------- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 438 ++++-------------- .../components/fields/RepoUrlPicker/index.ts | 2 +- .../components/fields/RepoUrlPicker/utils.ts | 71 +++ 4 files changed, 164 insertions(+), 507 deletions(-) delete mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/NewRepoUrlPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/NewRepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/NewRepoUrlPicker.tsx deleted file mode 100644 index 50a8101165..0000000000 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/NewRepoUrlPicker.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 { useApi } from '@backstage/core-plugin-api'; -import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import React, { useEffect, useState } from 'react'; -import { GithubRepoPicker } from './GithubRepoPicker'; -import { GitlabRepoPicker } from './GitlabRepoPicker'; -import { AzureRepoPicker } from './AzureRepoPicker'; -import { BitbucketRepoPicker } from './BitbucketRepoPicker'; -import { CustomFieldExtension } from '../../../extensions'; -import { RepoUrlPickerHost } from './RepoUrlPickerHost'; - -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 = ({ - uiSchema, - onChange, - rawErrors, - formData, -}: CustomFieldExtension) => { - const [state, setState] = useState<{ - host?: string; - owner?: string; - repo?: string; - organization?: string; - workspace?: string; - project?: string; - }>({}); - const integrationApi = useApi(scmIntegrationsApiRef); - - const allowedHosts = uiSchema?.['ui:options']?.allowedHosts ?? []; - const allowedOwners = uiSchema?.['ui:options']?.allowedOwners ?? []; - - useEffect(() => { - onChange(serializeFormData(state)); - }, [state, onChange]); - - return ( - <> - setState({ host })} - rawErrors={rawErrors} - /> - {state.host && integrationApi.byHost(state.host)?.type === 'github' && ( - - setState(prevState => ({ ...prevState, repo })) - } - onOwnerChange={owner => - setState(prevState => ({ ...prevState, owner })) - } - /> - )} - {state.host && integrationApi.byHost(state.host)?.type === 'gitlab' && ( - - setState(prevState => ({ ...prevState, repo })) - } - onOwnerChange={owner => - setState(prevState => ({ ...prevState, owner })) - } - /> - )} - {state.host && - integrationApi.byHost(state.host)?.type === 'bitbucket' && ( - - setState(prevState => ({ ...prevState, repo })) - } - onProjectChange={project => - setState(prevState => ({ ...prevState, project })) - } - onWorkspaceChange={workspace => - setState(prevState => ({ ...prevState, workspace })) - } - /> - )} - {state.host && integrationApi.byHost(state.host)?.type === 'azure' && ( - - setState(prevState => ({ ...prevState, owner })) - } - onRepoNameChange={repo => - setState(prevState => ({ ...prevState, repo })) - } - onOrgChange={org => - setState(prevState => ({ ...prevState, organization: org })) - } - /> - )} - - ); -}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 85fe992935..ba85bdaeb1 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -13,371 +13,117 @@ * 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 } from 'react'; +import { GithubRepoPicker } from './GithubRepoPicker'; +import { GitlabRepoPicker } from './GitlabRepoPicker'; +import { AzureRepoPicker } from './AzureRepoPicker'; +import { BitbucketRepoPicker } from './BitbucketRepoPicker'; +import { CustomFieldExtension } from '../../../extensions'; +import { RepoUrlPickerHost } from './RepoUrlPickerHost'; +import { splitFormData, serializeFormData } from './utils'; -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 }; -} - -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 interface RepoUrlPickerUiOptions { + allowedHosts?: string[]; + allowedOwners?: string[]; } export const RepoUrlPicker = ({ - onChange, uiSchema, + onChange, rawErrors, formData, -}: FieldProps) => { - const scaffolderApi = useApi(scaffolderApiRef); +}: CustomFieldExtension) => { + const [state, setState] = useState<{ + host?: string; + owner?: string; + repo?: string; + organization?: string; + workspace?: string; + project?: string; + }>(splitFormData(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 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 allowedHosts = uiSchema?.['ui:options']?.allowedHosts ?? []; + const allowedOwners = uiSchema?.['ui:options']?.allowedOwners ?? []; useEffect(() => { - if (host === undefined && integrations?.length) { - onChange( - serializeFormData({ - host: integrations[0].host, - owner, - repo, - organization, - workspace, - project, - }), - ); - } - }, [ - onChange, - integrations, - host, - owner, - repo, - organization, - workspace, - project, - ]); - - if (loading) { - return ; - } - - 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' }]; + onChange(serializeFormData(state)); + }, [state, onChange]); return ( <> - 0 && !host} - > - + setState(prevState => ({ ...prevState, repo })) + } + onOwnerChange={owner => + setState(prevState => ({ ...prevState, owner })) + } + /> + )} + {state.host && + integrationApi.byHost(state.host)?.type === 'bitbucket' && ( + + setState(prevState => ({ ...prevState, repo })) + } + onProjectChange={project => + setState(prevState => ({ ...prevState, project })) + } + onWorkspaceChange={workspace => + setState(prevState => ({ ...prevState, workspace })) + } /> - The name of the organization - - )} - {host && integrationApi.byHost(host)?.type === 'bitbucket' && ( - <> - {/* Show this for bitbucket.org only */} - {host === 'bitbucket.org' && ( - 0 && !workspace} - > - Workspace - - - The workspace where the repository will be created - - - )} - 0 && !project} - > - Project - - - The project where the repository will be created - - - - )} - {/* Show this for all hosts except bitbucket */} - {host && - integrationApi.byHost(host)?.type !== 'bitbucket' && - !allowedOwners && ( - <> - 0 && !owner} - > - Owner - - - The organization, user or project that this repo will belong to - - - )} - {/* Show this for all hosts except bitbucket where allowed owner is set */} - {host && - integrationApi.byHost(host)?.type !== 'bitbucket' && - allowedOwners && ( - <> - 0 && !owner} - > - - The name of the repository - + {state.host && integrationApi.byHost(state.host)?.type === 'azure' && ( + + setState(prevState => ({ ...prevState, owner })) + } + onRepoNameChange={repo => + setState(prevState => ({ ...prevState, repo })) + } + onOrgChange={org => + setState(prevState => ({ ...prevState, organization: org })) + } + /> + )} ); }; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index 99bcff332b..7e14d6d62b 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { RepoUrlPicker } from './NewRepoUrlPicker'; +export { RepoUrlPicker } from './RepoUrlPicker'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts new file mode 100644 index 0000000000..8ef109f4c8 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts @@ -0,0 +1,71 @@ +/* + * 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. + */ +export 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 function splitFormData(url: string | undefined) { + 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') || undefined; + repo = 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, repo, organization, workspace, project }; +} From cd05442ed28aad28b617fae0358f4cfb26bcf414 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 22 Dec 2021 09:53:56 +0100 Subject: [PATCH 07/22] chore: added changeset Signed-off-by: blam --- .changeset/tame-crabs-push.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tame-crabs-push.md diff --git a/.changeset/tame-crabs-push.md b/.changeset/tame-crabs-push.md new file mode 100644 index 0000000000..02c0b874e1 --- /dev/null +++ b/.changeset/tame-crabs-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Refactoring the `RepoUrlPicker` into separate provider components to encapsulate provider nonsense From 269f4dd307d503ffbd3cdc3780895377bca6f53b Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 24 Dec 2021 15:08:13 +0100 Subject: [PATCH 08/22] chore: added some tests for the hosts dropdown Signed-off-by: blam --- .../RepoUrlPicker/RepoUrlPickerHost.test.tsx | 99 +++++++++++++++++++ .../RepoUrlPicker/RepoUrlPickerHost.tsx | 8 +- 2 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.test.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.test.tsx new file mode 100644 index 0000000000..e151ddbbc7 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.test.tsx @@ -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( + + + , + ); + + 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( + + + , + ); + + 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( + + + , + ); + + fireEvent.mouseDown(getByTestId('select')); + expect(getByText('gitlab.com')).toBeInTheDocument(); + + const listbox = within(getByRole('combobox')); + + expect(listbox.getAllByRole('option')).toHaveLength(2); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx index 8e270610bc..979e33266c 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx @@ -14,12 +14,7 @@ * limitations under the License. */ import React, { useEffect } from 'react'; -import { - Progress, - Select, - SelectedItems, - SelectItem, -} from '@backstage/core-components'; +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'; @@ -75,6 +70,7 @@ export const RepoUrlPickerHost = ({ onChange={s => onChange(String(Array.isArray(s) ? s[0] : s))} selected={host} items={hostsOptions} + data-testid="host-select" /> From dd17fa9e31c352c9e2165ee136da2a7e4de69a26 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 28 Dec 2021 23:02:39 +0100 Subject: [PATCH 09/22] chore: updating the api-report with the new type Signed-off-by: blam --- plugins/scaffolder/api-report.md | 16 ++++++++++++++-- .../src/components/fields/RepoUrlPicker/index.ts | 1 + plugins/scaffolder/src/index.ts | 6 +++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 0c3904adf6..28dc0b7e40 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -36,6 +36,17 @@ export function createScaffolderFieldExtension( options: FieldExtensionOptions, ): Extension<() => null>; +// Warning: (ae-missing-release-tag) "CustomFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface CustomFieldExtension + extends FieldProps { + // (undocumented) + uiSchema: { + 'ui:options'?: UiOptions; + }; +} + // 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) @@ -137,15 +148,16 @@ export const OwnerPicker: ({ // @public (undocumented) export const OwnerPickerFieldExtension: () => null; +// Warning: (ae-forgotten-export) The symbol "RepoUrlPickerUiOptions" needs to be exported by the entry point index.d.ts // 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, + onChange, rawErrors, formData, -}: FieldProps) => JSX.Element; +}: CustomFieldExtension) => 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) // diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index 7e14d6d62b..2fdbf0aac9 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ export { RepoUrlPicker } from './RepoUrlPicker'; +export type { RepoUrlPickerUiOptions } from './RepoUrlPicker'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 3851b2151b..86f684cb2d 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -26,7 +26,11 @@ export { createScaffolderFieldExtension, ScaffolderFieldExtensions, } from './extensions'; -export type { CustomFieldValidator, FieldExtensionOptions } from './extensions'; +export type { + CustomFieldValidator, + FieldExtensionOptions, + CustomFieldExtension, +} from './extensions'; export { EntityPickerFieldExtension, EntityNamePickerFieldExtension, From 381184c4388bd2e000634cfb0ab6aea78e52323f Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 3 Jan 2022 17:09:00 +0100 Subject: [PATCH 10/22] chore: change the import to of `react-use` things to use explicit import Signed-off-by: blam --- .../src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx index 979e33266c..1e201f1afe 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx @@ -19,7 +19,7 @@ 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'; +import useAsync from 'react-use/lib/useAsync'; export const RepoUrlPickerHost = ({ host, From 66be3c5fd4327689ff02443bf802ac3ef7bed0da Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 3 Jan 2022 17:30:49 +0100 Subject: [PATCH 11/22] chore: added tests for util files Signed-off-by: blam --- .../fields/RepoUrlPicker/utils.test.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts new file mode 100644 index 0000000000..e6065ef737 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts @@ -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 { splitFormData, serializeFormData } from './utils'; + +describe('utils', () => { + describe('serializeFormData', () => { + it('should return undefined when host is not set', () => { + expect(serializeFormData({})).toBeUndefined(); + }); + + it('should set the correct owner and repo', () => { + expect( + serializeFormData({ + host: 'github.com', + owner: 'owner', + repo: 'backstage', + }), + ).toBe('github.com?owner=owner&repo=backstage'); + }); + + it('should set correct other options', () => { + expect( + serializeFormData({ + 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( + serializeFormData({ + host: 'github.com', + owner: 'owner', + repo: 'backstage', + organization: 'organization', + workspace: 'workspace', + project: 'backstage', + }), + ).toBe( + 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage', + ); + }); + }); + + describe('splitFormData', () => { + it('should parse a complete string', () => { + expect( + splitFormData( + 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage', + ), + ).toEqual({ + host: 'github.com', + owner: 'owner', + repo: 'backstage', + organization: 'organization', + workspace: 'workspace', + project: 'backstage', + }); + }); + }); +}); From 9f4c552157ab9a7875c76dac3b195a84f29eba60 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 7 Jan 2022 18:10:11 +0100 Subject: [PATCH 12/22] chore: some code review comments Signed-off-by: blam --- .../src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx | 4 ++-- plugins/scaffolder/src/extensions/index.tsx | 4 ++-- plugins/scaffolder/src/extensions/types.ts | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index ba85bdaeb1..fea96dfe7f 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -20,7 +20,7 @@ import { GithubRepoPicker } from './GithubRepoPicker'; import { GitlabRepoPicker } from './GitlabRepoPicker'; import { AzureRepoPicker } from './AzureRepoPicker'; import { BitbucketRepoPicker } from './BitbucketRepoPicker'; -import { CustomFieldExtension } from '../../../extensions'; +import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; import { splitFormData, serializeFormData } from './utils'; @@ -34,7 +34,7 @@ export const RepoUrlPicker = ({ onChange, rawErrors, formData, -}: CustomFieldExtension) => { +}: FieldExtensionComponentProps) => { const [state, setState] = useState<{ host?: string; owner?: string; diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 9d5cf4f0b4..f9c167a6b0 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { CustomFieldValidator, FieldExtensionOptions, - CustomFieldExtension, + FieldExtensionComponentProps, } from './types'; import { Extension, attachComponentData } from '@backstage/core-plugin-api'; @@ -53,7 +53,7 @@ attachComponentData( export type { CustomFieldValidator, FieldExtensionOptions, - CustomFieldExtension, + FieldExtensionComponentProps, }; export { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from './default'; diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 12339f5be7..2fdb80e684 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -30,8 +30,10 @@ export type FieldExtensionOptions = { validation?: CustomFieldValidator; }; -export interface CustomFieldExtension - extends FieldProps { +export interface FieldExtensionComponentProps< + ReturnValue, + UiOptions extends {} = {}, +> extends FieldProps { uiSchema: { 'ui:options'?: UiOptions; }; From be5fc422f72decd9e168eb59d855191f9cdd1e60 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 7 Jan 2022 18:24:48 +0100 Subject: [PATCH 13/22] chore: reworking some more stuff in the code review, better naming Signed-off-by: blam --- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 6 +++--- .../fields/RepoUrlPicker/RepoUrlPickerHost.tsx | 2 ++ .../fields/RepoUrlPicker/utils.test.ts | 16 ++++++++-------- .../src/components/fields/RepoUrlPicker/utils.ts | 11 ++++++++--- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index fea96dfe7f..cdce9512a9 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -22,7 +22,7 @@ import { AzureRepoPicker } from './AzureRepoPicker'; import { BitbucketRepoPicker } from './BitbucketRepoPicker'; import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; -import { splitFormData, serializeFormData } from './utils'; +import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; export interface RepoUrlPickerUiOptions { allowedHosts?: string[]; @@ -42,14 +42,14 @@ export const RepoUrlPicker = ({ organization?: string; workspace?: string; project?: string; - }>(splitFormData(formData)); + }>(parseRepoPickerUrl(formData)); const integrationApi = useApi(scmIntegrationsApiRef); const allowedHosts = uiSchema?.['ui:options']?.allowedHosts ?? []; const allowedOwners = uiSchema?.['ui:options']?.allowedOwners ?? []; useEffect(() => { - onChange(serializeFormData(state)); + onChange(serializeRepoPickerUrl(state)); }, [state, onChange]); return ( diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx index 1e201f1afe..15777fde81 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx @@ -42,6 +42,8 @@ export const RepoUrlPickerHost = ({ 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]); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts index e6065ef737..2771d0faa5 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { splitFormData, serializeFormData } from './utils'; +import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; describe('utils', () => { - describe('serializeFormData', () => { + describe('serializeRepoPickerUrl', () => { it('should return undefined when host is not set', () => { - expect(serializeFormData({})).toBeUndefined(); + expect(serializeRepoPickerUrl({})).toBeUndefined(); }); it('should set the correct owner and repo', () => { expect( - serializeFormData({ + serializeRepoPickerUrl({ host: 'github.com', owner: 'owner', repo: 'backstage', @@ -33,7 +33,7 @@ describe('utils', () => { it('should set correct other options', () => { expect( - serializeFormData({ + serializeRepoPickerUrl({ host: 'github.com', organization: 'organization', workspace: 'workspace', @@ -46,7 +46,7 @@ describe('utils', () => { it('should set all correct options', () => { expect( - serializeFormData({ + serializeRepoPickerUrl({ host: 'github.com', owner: 'owner', repo: 'backstage', @@ -60,10 +60,10 @@ describe('utils', () => { }); }); - describe('splitFormData', () => { + describe('parseRepoPickerUrl', () => { it('should parse a complete string', () => { expect( - splitFormData( + parseRepoPickerUrl( 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage', ), ).toEqual({ diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts index 8ef109f4c8..66c010ec3d 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export function serializeFormData(data: { + +type RepoUrlPickerOptions = { host?: string; owner?: string; repo?: string; organization?: string; workspace?: string; project?: string; -}) { +}; + +export function serializeRepoPickerUrl(data: RepoUrlPickerOptions) { if (!data.host) { return undefined; } @@ -45,7 +48,9 @@ export function serializeFormData(data: { return `${data.host}?${params.toString()}`; } -export function splitFormData(url: string | undefined) { +export function parseRepoPickerUrl( + url: string | undefined, +): RepoUrlPickerOptions { let host = undefined; let owner = undefined; let repo = undefined; From abdfe612c01c20be79fc95f14836f8dc0065ba20 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 7 Jan 2022 18:56:08 +0100 Subject: [PATCH 14/22] chore: updating the api-report Signed-off-by: blam --- plugins/scaffolder/api-report.md | 32 ++++++++++------------ plugins/scaffolder/src/extensions/types.ts | 17 ++++++++++++ plugins/scaffolder/src/index.ts | 2 +- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 28dc0b7e40..63eea46fac 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -36,20 +36,7 @@ export function createScaffolderFieldExtension( options: FieldExtensionOptions, ): Extension<() => null>; -// Warning: (ae-missing-release-tag) "CustomFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface CustomFieldExtension - extends FieldProps { - // (undocumented) - uiSchema: { - 'ui:options'?: UiOptions; - }; -} - -// 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 = | ((data: T, field: FieldValidation) => void) | (( @@ -107,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 { + // (undocumented) + uiSchema: { + 'ui:options'?: UiOptions; + }; +} + +// @public export type FieldExtensionOptions = { name: string; component: (props: FieldProps) => JSX.Element | null; @@ -157,7 +153,7 @@ export const RepoUrlPicker: ({ onChange, rawErrors, formData, -}: CustomFieldExtension) => JSX.Element; +}: FieldExtensionComponentProps) => 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) // diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 2fdb80e684..e1da9889e7 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -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 = | ((data: T, field: FieldValidation) => void) | (( @@ -24,12 +29,24 @@ export type CustomFieldValidator = context: { apiHolder: ApiHolder }, ) => void); +/** + * Type for the Custom Field Extension with the + * name and components and validation function. + * + * @public + */ export type FieldExtensionOptions = { name: string; component: (props: FieldProps) => JSX.Element | null; validation?: CustomFieldValidator; }; +/** + * Type for field extensions and being able to type + * incoming props easier. + * + * @public + */ export interface FieldExtensionComponentProps< ReturnValue, UiOptions extends {} = {}, diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 86f684cb2d..57ce4df516 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -29,7 +29,7 @@ export { export type { CustomFieldValidator, FieldExtensionOptions, - CustomFieldExtension, + FieldExtensionComponentProps, } from './extensions'; export { EntityPickerFieldExtension, From fbef4663f7464a4536731fa04cb75bb11a0bee05 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 10 Jan 2022 07:10:10 +0100 Subject: [PATCH 15/22] chore: starting to add some tests for the GithubRepoUrlPicker Signed-off-by: blam --- .../RepoUrlPicker/GithubRepoPicker.test.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx new file mode 100644 index 0000000000..0dfa2f28e8 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -0,0 +1,36 @@ +/* + * 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 } from '@testing-library/react'; + +describe('GitubRepoPicker', () => { + it('renders a select if there is a list of allowed owners', () => { + const allowedOwners = ['owner1', 'owner2']; + const { findByText } = render( + , + ); + + expect(findByText('owner1')).toBeInTheDocument(); + }); +}); From 5688027163aff7b7097e7c6077b60b28220bd966 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 12 Jan 2022 14:12:22 +0100 Subject: [PATCH 16/22] chore: fix tests for GithubRepoPicker Signed-off-by: blam --- .../fields/RepoUrlPicker/GithubRepoPicker.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index 0dfa2f28e8..c11f7a52a8 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -19,7 +19,7 @@ import { GithubRepoPicker } from './GithubRepoPicker'; import { render } from '@testing-library/react'; describe('GitubRepoPicker', () => { - it('renders a select if there is a list of allowed owners', () => { + it('renders a select if there is a list of allowed owners', async () => { const allowedOwners = ['owner1', 'owner2']; const { findByText } = render( { />, ); - expect(findByText('owner1')).toBeInTheDocument(); + expect(await findByText('owner1')).toBeInTheDocument(); + expect(await findByText('owner2')).toBeInTheDocument(); }); }); From 142d65b1d788a6af6c864026525fe2aefcc58c62 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 13 Jan 2022 14:43:42 +0100 Subject: [PATCH 17/22] chore: reworking tests and fixing up some of the types for the repo pickers Signed-off-by: blam --- .../RepoUrlPicker/GithubRepoPicker.test.tsx | 104 +++++++++++++++--- .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 2 +- .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 2 +- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 14 ++- 4 files changed, 104 insertions(+), 18 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index c11f7a52a8..3f1105badc 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -16,22 +16,98 @@ import React from 'react'; import { GithubRepoPicker } from './GithubRepoPicker'; -import { render } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; describe('GitubRepoPicker', () => { - it('renders a select if there is a list of allowed owners', async () => { - const allowedOwners = ['owner1', 'owner2']; - const { findByText } = render( - , - ); + describe('owner field', () => { + it('renders a select if there is a list of allowed owners', async () => { + const allowedOwners = ['owner1', 'owner2']; + const { findByText } = render( + , + ); - expect(await findByText('owner1')).toBeInTheDocument(); - expect(await findByText('owner2')).toBeInTheDocument(); + expect(await findByText('owner1')).toBeInTheDocument(); + expect(await findByText('owner2')).toBeInTheDocument(); + }); + + it('calls onOwnerChange when the owner is changed to a different owner', async () => { + const onOwnerChange = jest.fn(); + const allowedOwners = ['owner1', 'owner2']; + const { getByRole } = render( + , + ); + + await fireEvent.change(getByRole('combobox'), { + target: { value: 'owner2' }, + }); + + expect(onOwnerChange).toHaveBeenCalledWith('owner2'); + }); + + it('is disabled picked when only one allowed owner', () => { + const onOwnerChange = jest.fn(); + const allowedOwners = ['owner1']; + const { getByRole } = render( + , + ); + + expect(getByRole('combobox')).toBeDisabled(); + }); + + it('should display free text if no allowed owners are passed', async () => { + const onOwnerChange = jest.fn(); + const { getAllByRole } = render( + , + ); + + const ownerField = getAllByRole('textbox')[0]; + fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } }); + + expect(onOwnerChange).toHaveBeenCalledWith('my-mock-owner'); + }); + }); + + describe('repo name', () => { + it('should render free text field for input of repo name', () => { + const onRepoNameChange = jest.fn(); + const { getAllByRole } = render( + , + ); + + const repoNameField = getAllByRole('textbox')[1]; + fireEvent.change(repoNameField, { + target: { value: 'my-mock-repo-name' }, + }); + + expect(onRepoNameChange).toHaveBeenCalledWith('my-mock-repo-name'); + }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 53126420f3..627027d130 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -30,7 +30,7 @@ export const GithubRepoPicker = ({ }: { onOwnerChange: (owner: string) => void; onRepoNameChange: (name: string) => void; - allowedOwners: string[]; + allowedOwners?: string[]; owner?: string; repoName?: string; rawErrors: string[]; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index 2aecc76fbc..ae76c29555 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -30,7 +30,7 @@ export const GitlabRepoPicker = ({ }: { onOwnerChange: (owner: string) => void; onRepoNameChange: (name: string) => void; - allowedOwners: string[]; + allowedOwners?: string[]; owner?: string; repoName?: string; rawErrors: string[]; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index cdce9512a9..68df677274 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -15,7 +15,7 @@ */ import { useApi } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { GithubRepoPicker } from './GithubRepoPicker'; import { GitlabRepoPicker } from './GitlabRepoPicker'; import { AzureRepoPicker } from './AzureRepoPicker'; @@ -46,12 +46,22 @@ export const RepoUrlPicker = ({ const integrationApi = useApi(scmIntegrationsApiRef); const allowedHosts = uiSchema?.['ui:options']?.allowedHosts ?? []; - const allowedOwners = uiSchema?.['ui:options']?.allowedOwners ?? []; + const allowedOwners = useMemo( + () => uiSchema?.['ui:options']?.allowedOwners ?? [], + [uiSchema], + ); useEffect(() => { 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] })); + } + }, [setState, allowedOwners]); + return ( <> Date: Thu, 13 Jan 2022 17:34:59 +0100 Subject: [PATCH 18/22] chore: simplify the contract between the different provider pickers Signed-off-by: blam --- .../fields/RepoUrlPicker/AzureRepoPicker.tsx | 31 ++++---- .../RepoUrlPicker/BitbucketRepoPicker.tsx | 26 +++---- .../RepoUrlPicker/GithubRepoPicker.test.tsx | 42 +++++------ .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 23 +++--- .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 25 ++++--- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 72 ++++++------------- .../components/fields/RepoUrlPicker/types.ts | 23 ++++++ .../fields/RepoUrlPicker/utils.test.ts | 6 +- .../components/fields/RepoUrlPicker/utils.ts | 23 +++--- 9 files changed, 121 insertions(+), 150 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx index df6b105368..1a61dad251 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -13,41 +13,36 @@ * 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 = ({ - onOrgChange, - onOwnerChange, - onRepoNameChange, rawErrors, - org, - owner, - repoName, + state, + onChange, }: { - onOrgChange: (org: string) => void; - onOwnerChange: (owner: string) => void; - onRepoNameChange: (name: string) => void; - owner?: string; - org?: string; - repoName?: string; + state: RepoUrlPickerState; + onChange: (state: RepoUrlPickerState) => void; rawErrors: string[]; }) => { + const { organization, repoName, owner } = state; return ( <> 0 && !org} + error={rawErrors?.length > 0 && !organization} > Organization onOrgChange(e.target.value)} - value={org} + onChange={e => onChange({ organization: e.target.value })} + value={organization} /> The organization that this repo will belong to @@ -61,7 +56,7 @@ export const AzureRepoPicker = ({ Owner onOwnerChange(e.target.value)} + onChange={e => onChange({ owner: e.target.value })} value={owner} /> The Owner that this repo will belong to @@ -74,7 +69,7 @@ export const AzureRepoPicker = ({ Repository onRepoNameChange(e.target.value)} + onChange={e => onChange({ repoName: e.target.value })} value={repoName} /> The name of the repository diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 7712a490b2..1240a4e15b 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -18,26 +18,18 @@ 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 = ({ - onProjectChange, - onWorkspaceChange, - onRepoNameChange, + onChange, rawErrors, - workspace, - project, - host, - repoName, + state, }: { - onProjectChange: (owner: string) => void; - onWorkspaceChange: (name: string) => void; - onRepoNameChange: (name: string) => void; - workspace?: string; - project?: string; - repoName?: string; - host: string; + onChange: (state: RepoUrlPickerState) => void; + state: RepoUrlPickerState; rawErrors: string[]; }) => { + const { host, workspace, project, repoName } = state; return ( <> {host === 'bitbucket.org' && ( @@ -49,7 +41,7 @@ export const BitbucketRepoPicker = ({ Workspace onWorkspaceChange(e.target.value)} + onChange={e => onChange({ workspace: e.target.value })} value={workspace} /> @@ -65,7 +57,7 @@ export const BitbucketRepoPicker = ({ Project onProjectChange(e.target.value)} + onChange={e => onChange({ project: e.target.value })} value={project} /> @@ -80,7 +72,7 @@ export const BitbucketRepoPicker = ({ Repository onRepoNameChange(e.target.value)} + onChange={e => onChange({ repoName: e.target.value })} value={repoName} /> The name of the repository diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index 3f1105badc..4a17016959 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -24,10 +24,9 @@ describe('GitubRepoPicker', () => { const allowedOwners = ['owner1', 'owner2']; const { findByText } = render( , ); @@ -36,15 +35,14 @@ describe('GitubRepoPicker', () => { expect(await findByText('owner2')).toBeInTheDocument(); }); - it('calls onOwnerChange when the owner is changed to a different owner', async () => { - const onOwnerChange = jest.fn(); + it('calls onChange when the owner is changed to a different owner', async () => { + const onChange = jest.fn(); const allowedOwners = ['owner1', 'owner2']; const { getByRole } = render( , ); @@ -53,18 +51,17 @@ describe('GitubRepoPicker', () => { target: { value: 'owner2' }, }); - expect(onOwnerChange).toHaveBeenCalledWith('owner2'); + expect(onChange).toHaveBeenCalledWith({ owner: 'owner2' }); }); it('is disabled picked when only one allowed owner', () => { - const onOwnerChange = jest.fn(); + const onChange = jest.fn(); const allowedOwners = ['owner1']; const { getByRole } = render( , ); @@ -73,32 +70,29 @@ describe('GitubRepoPicker', () => { }); it('should display free text if no allowed owners are passed', async () => { - const onOwnerChange = jest.fn(); + const onChange = jest.fn(); const { getAllByRole } = render( , ); - const ownerField = getAllByRole('textbox')[0]; fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } }); - expect(onOwnerChange).toHaveBeenCalledWith('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 onRepoNameChange = jest.fn(); + const onChange = jest.fn(); const { getAllByRole } = render( , ); @@ -107,7 +101,7 @@ describe('GitubRepoPicker', () => { target: { value: 'my-mock-repo-name' }, }); - expect(onRepoNameChange).toHaveBeenCalledWith('my-mock-repo-name'); + expect(onChange).toHaveBeenCalledWith({ repoName: 'my-mock-repo-name' }); }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 627027d130..22d13cdcdb 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -19,26 +19,25 @@ 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 = ({ - onOwnerChange, - onRepoNameChange, allowedOwners = [], rawErrors, - owner, - repoName, + state, + onChange, }: { - onOwnerChange: (owner: string) => void; - onRepoNameChange: (name: string) => void; allowedOwners?: string[]; - owner?: string; - repoName?: string; rawErrors: string[]; + state: RepoUrlPickerState; + onChange: (state: RepoUrlPickerState) => void; }) => { const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; + const { owner, repoName } = state; + return ( <> onOwnerChange(String(Array.isArray(s) ? s[0] : s))} + onChange={s => + onChange({ owner: String(Array.isArray(s) ? s[0] : s) }) + } disabled={allowedOwners.length === 1} selected={owner} items={ownerItems} @@ -60,7 +61,7 @@ export const GithubRepoPicker = ({ Owner onOwnerChange(e.target.value)} + onChange={e => onChange({ owner: e.target.value })} value={owner} /> @@ -77,7 +78,7 @@ export const GithubRepoPicker = ({ Repository onRepoNameChange(e.target.value)} + onChange={e => onChange({ repoName: e.target.value })} value={repoName} /> The name of the repository diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index ae76c29555..0d0ad26ee1 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -19,26 +19,25 @@ 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 = ({ - onOwnerChange, - onRepoNameChange, allowedOwners = [], rawErrors, - owner, - repoName, + state, + onChange, }: { - onOwnerChange: (owner: string) => void; - onRepoNameChange: (name: string) => void; allowedOwners?: string[]; - owner?: string; - repoName?: string; + state: RepoUrlPickerState; + onChange: (state: RepoUrlPickerState) => void; rawErrors: string[]; }) => { const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; + const { owner, repoName } = state; + return ( <> onOwnerChange(String(Array.isArray(s) ? s[0] : s))} + onChange={selected => + onChange({ + owner: String(Array.isArray(selected) ? selected[0] : selected), + }) + } disabled={allowedOwners.length === 1} selected={owner} items={ownerItems} @@ -60,7 +63,7 @@ export const GitlabRepoPicker = ({ Owner onOwnerChange(e.target.value)} + onChange={e => onChange({ owner: e.target.value })} value={owner} /> @@ -77,7 +80,7 @@ export const GitlabRepoPicker = ({ Repository onRepoNameChange(e.target.value)} + onChange={e => onChange({ repoName: e.target.value })} value={repoName} /> The name of the repository diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 68df677274..65b1f8bf61 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -15,7 +15,7 @@ */ import { useApi } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { GithubRepoPicker } from './GithubRepoPicker'; import { GitlabRepoPicker } from './GitlabRepoPicker'; import { AzureRepoPicker } from './AzureRepoPicker'; @@ -23,6 +23,7 @@ import { BitbucketRepoPicker } from './BitbucketRepoPicker'; import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; +import { RepoUrlPickerState } from './types'; export interface RepoUrlPickerUiOptions { allowedHosts?: string[]; @@ -35,14 +36,9 @@ export const RepoUrlPicker = ({ rawErrors, formData, }: FieldExtensionComponentProps) => { - const [state, setState] = useState<{ - host?: string; - owner?: string; - repo?: string; - organization?: string; - workspace?: string; - project?: string; - }>(parseRepoPickerUrl(formData)); + const [state, setState] = useState( + parseRepoPickerUrl(formData), + ); const integrationApi = useApi(scmIntegrationsApiRef); const allowedHosts = uiSchema?.['ui:options']?.allowedHosts ?? []; @@ -62,76 +58,50 @@ export const RepoUrlPicker = ({ } }, [setState, allowedOwners]); + const updateLocalState = useCallback( + (newState: RepoUrlPickerState) => { + setState(prevState => ({ ...prevState, ...newState })); + }, + [setState], + ); + return ( <> setState({ host })} + onChange={host => setState(prevState => ({ ...prevState, host }))} rawErrors={rawErrors} /> {state.host && integrationApi.byHost(state.host)?.type === 'github' && ( - setState(prevState => ({ ...prevState, repo })) - } - onOwnerChange={owner => - setState(prevState => ({ ...prevState, owner })) - } + state={state} + onChange={updateLocalState} /> )} {state.host && integrationApi.byHost(state.host)?.type === 'gitlab' && ( - setState(prevState => ({ ...prevState, repo })) - } - onOwnerChange={owner => - setState(prevState => ({ ...prevState, owner })) - } + state={state} + onChange={updateLocalState} /> )} {state.host && integrationApi.byHost(state.host)?.type === 'bitbucket' && ( - setState(prevState => ({ ...prevState, repo })) - } - onProjectChange={project => - setState(prevState => ({ ...prevState, project })) - } - onWorkspaceChange={workspace => - setState(prevState => ({ ...prevState, workspace })) - } + state={state} + onChange={updateLocalState} /> )} {state.host && integrationApi.byHost(state.host)?.type === 'azure' && ( - setState(prevState => ({ ...prevState, owner })) - } - onRepoNameChange={repo => - setState(prevState => ({ ...prevState, repo })) - } - onOrgChange={org => - setState(prevState => ({ ...prevState, organization: org })) - } + state={state} + onChange={updateLocalState} /> )} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts new file mode 100644 index 0000000000..5e979e789e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts @@ -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; +} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts index 2771d0faa5..c90b357cf1 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts @@ -26,7 +26,7 @@ describe('utils', () => { serializeRepoPickerUrl({ host: 'github.com', owner: 'owner', - repo: 'backstage', + repoName: 'backstage', }), ).toBe('github.com?owner=owner&repo=backstage'); }); @@ -49,7 +49,7 @@ describe('utils', () => { serializeRepoPickerUrl({ host: 'github.com', owner: 'owner', - repo: 'backstage', + repoName: 'backstage', organization: 'organization', workspace: 'workspace', project: 'backstage', @@ -69,7 +69,7 @@ describe('utils', () => { ).toEqual({ host: 'github.com', owner: 'owner', - repo: 'backstage', + repoName: 'backstage', organization: 'organization', workspace: 'workspace', project: 'backstage', diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts index 66c010ec3d..d1f2302d3e 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts @@ -14,16 +14,9 @@ * limitations under the License. */ -type RepoUrlPickerOptions = { - host?: string; - owner?: string; - repo?: string; - organization?: string; - workspace?: string; - project?: string; -}; +import { RepoUrlPickerState } from './types'; -export function serializeRepoPickerUrl(data: RepoUrlPickerOptions) { +export function serializeRepoPickerUrl(data: RepoUrlPickerState) { if (!data.host) { return undefined; } @@ -32,8 +25,8 @@ export function serializeRepoPickerUrl(data: RepoUrlPickerOptions) { if (data.owner) { params.set('owner', data.owner); } - if (data.repo) { - params.set('repo', data.repo); + if (data.repoName) { + params.set('repo', data.repoName); } if (data.organization) { params.set('organization', data.organization); @@ -50,10 +43,10 @@ export function serializeRepoPickerUrl(data: RepoUrlPickerOptions) { export function parseRepoPickerUrl( url: string | undefined, -): RepoUrlPickerOptions { +): RepoUrlPickerState { let host = undefined; let owner = undefined; - let repo = undefined; + let repoName = undefined; let organization = undefined; let workspace = undefined; let project = undefined; @@ -63,7 +56,7 @@ export function parseRepoPickerUrl( const parsed = new URL(`https://${url}`); host = parsed.host; owner = parsed.searchParams.get('owner') || undefined; - repo = parsed.searchParams.get('repo') || 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; @@ -72,5 +65,5 @@ export function parseRepoPickerUrl( /* ok */ } - return { host, owner, repo, organization, workspace, project }; + return { host, owner, repoName, organization, workspace, project }; } From 1b026578f00cc6c5a17a06bd7676916383530cc9 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 13 Jan 2022 17:41:14 +0100 Subject: [PATCH 19/22] chore: export some missing bits for the api-report.md Signed-off-by: blam --- plugins/scaffolder/api-report.md | 11 ++++++++++- plugins/scaffolder/src/index.ts | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 63eea46fac..bc9eeb914b 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -144,7 +144,6 @@ export const OwnerPicker: ({ // @public (undocumented) export const OwnerPickerFieldExtension: () => null; -// Warning: (ae-forgotten-export) The symbol "RepoUrlPickerUiOptions" needs to be exported by the entry point index.d.ts // 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) @@ -160,6 +159,16 @@ export const RepoUrlPicker: ({ // @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) diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 57ce4df516..3cf65d7a07 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -51,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'; From 33e2553ccc99a459fb7dd359b549ed61a2bc1694 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 14 Jan 2022 06:50:47 +0100 Subject: [PATCH 20/22] chore: added tests for the rest of the broken out components. Now I think we're ready to go... Signed-off-by: blam --- .../RepoUrlPicker/AzureRepoPicker.test.tsx | 76 +++++++++++++ .../fields/RepoUrlPicker/AzureRepoPicker.tsx | 4 +- .../BitbucketRepoPicker.test.tsx | 100 ++++++++++++++++ .../RepoUrlPicker/BitbucketRepoPicker.tsx | 4 +- .../RepoUrlPicker/GithubRepoPicker.test.tsx | 2 +- .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 4 +- .../RepoUrlPicker/GitlabRepoPicker.test.tsx | 107 ++++++++++++++++++ .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 4 +- 8 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.test.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.test.tsx new file mode 100644 index 0000000000..ffcf2920c1 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + const repoNameInput = getAllByRole('textbox')[2]; + + fireEvent.change(repoNameInput, { target: { value: 'repoName' } }); + + expect(onChange).toHaveBeenCalledWith({ repoName: 'repoName' }); + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx index 1a61dad251..584cce7eed 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx @@ -66,9 +66,9 @@ export const AzureRepoPicker = ({ required error={rawErrors?.length > 0 && !repoName} > - Repository + Repository onChange({ repoName: e.target.value })} value={repoName} /> diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx new file mode 100644 index 0000000000..17fe656006 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx @@ -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( + , + ); + + 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( + , + ); + + expect(getAllByRole('textbox')).toHaveLength(2); + }); + describe('workspace field', () => { + it('calls onChange when the workspace changes', () => { + const onChange = jest.fn(); + const { getAllByRole } = render( + , + ); + + 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( + , + ); + + 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( + , + ); + + const repoNameInput = getAllByRole('textbox')[2]; + + fireEvent.change(repoNameInput, { target: { value: 'test-repo' } }); + + expect(onChange).toHaveBeenCalledWith({ repoName: 'test-repo' }); + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 1240a4e15b..5b4f271d74 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -69,9 +69,9 @@ export const BitbucketRepoPicker = ({ required error={rawErrors?.length > 0 && !repoName} > - Repository + Repository onChange({ repoName: e.target.value })} value={repoName} /> diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index 4a17016959..2c80c40c22 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { GithubRepoPicker } from './GithubRepoPicker'; import { render, fireEvent } from '@testing-library/react'; -describe('GitubRepoPicker', () => { +describe('GithubRepoPicker', () => { describe('owner field', () => { it('renders a select if there is a list of allowed owners', async () => { const allowedOwners = ['owner1', 'owner2']; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 22d13cdcdb..b59d089a19 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -75,9 +75,9 @@ export const GithubRepoPicker = ({ required error={rawErrors?.length > 0 && !repoName} > - Repository + Repository onChange({ repoName: e.target.value })} value={repoName} /> diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx new file mode 100644 index 0000000000..49a91c116e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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( + , + ); + + expect(getByRole('combobox')).toBeDisabled(); + }); + + it('should display free text if no allowed owners are passed', async () => { + const onChange = jest.fn(); + const { getAllByRole } = render( + , + ); + 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( + , + ); + + const repoNameField = getAllByRole('textbox')[1]; + fireEvent.change(repoNameField, { + target: { value: 'my-mock-repo-name' }, + }); + + expect(onChange).toHaveBeenCalledWith({ repoName: 'my-mock-repo-name' }); + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index 0d0ad26ee1..0e4e7dcd8f 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -77,9 +77,9 @@ export const GitlabRepoPicker = ({ required error={rawErrors?.length > 0 && !repoName} > - Repository + Repository onChange({ repoName: e.target.value })} value={repoName} /> From c56c70cf7b80b42694be66c29e7d0b28e538ac56 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 18 Jan 2022 08:16:51 +0100 Subject: [PATCH 21/22] chore: fixing up code review comments for the RepoUrlPicker Signed-off-by: blam --- .../fields/RepoUrlPicker/AzureRepoPicker.tsx | 7 +--- .../RepoUrlPicker/BitbucketRepoPicker.tsx | 7 +--- .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 8 +--- .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 8 +--- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 39 ++++++++++--------- .../RepoUrlPicker/RepoUrlPickerHost.tsx | 8 +--- 6 files changed, 31 insertions(+), 46 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx index 584cce7eed..0efadf4017 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx @@ -21,15 +21,12 @@ import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; import { RepoUrlPickerState } from './types'; -export const AzureRepoPicker = ({ - rawErrors, - state, - onChange, -}: { +export const AzureRepoPicker = (props: { state: RepoUrlPickerState; onChange: (state: RepoUrlPickerState) => void; rawErrors: string[]; }) => { + const { rawErrors, state, onChange } = props; const { organization, repoName, owner } = state; return ( <> diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 5b4f271d74..64eb4b8dac 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -20,15 +20,12 @@ import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; import { RepoUrlPickerState } from './types'; -export const BitbucketRepoPicker = ({ - onChange, - rawErrors, - state, -}: { +export const BitbucketRepoPicker = (props: { onChange: (state: RepoUrlPickerState) => void; state: RepoUrlPickerState; rawErrors: string[]; }) => { + const { onChange, rawErrors, state } = props; const { host, workspace, project, repoName } = state; return ( <> diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index b59d089a19..13b57cb449 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -21,17 +21,13 @@ import InputLabel from '@material-ui/core/InputLabel'; import { Select, SelectItem } from '@backstage/core-components'; import { RepoUrlPickerState } from './types'; -export const GithubRepoPicker = ({ - allowedOwners = [], - rawErrors, - state, - onChange, -}: { +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' }]; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index 0e4e7dcd8f..1c63852866 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -21,17 +21,13 @@ import InputLabel from '@material-ui/core/InputLabel'; import { Select, SelectItem } from '@backstage/core-components'; import { RepoUrlPickerState } from './types'; -export const GitlabRepoPicker = ({ - allowedOwners = [], - rawErrors, - state, - onChange, -}: { +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' }]; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 65b1f8bf61..998fa41588 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -30,18 +30,19 @@ export interface RepoUrlPickerUiOptions { allowedOwners?: string[]; } -export const RepoUrlPicker = ({ - uiSchema, - onChange, - rawErrors, - formData, -}: FieldExtensionComponentProps) => { +export const RepoUrlPicker = ( + props: FieldExtensionComponentProps, +) => { + const { uiSchema, onChange, rawErrors, formData } = props; const [state, setState] = useState( parseRepoPickerUrl(formData), ); const integrationApi = useApi(scmIntegrationsApiRef); - const allowedHosts = uiSchema?.['ui:options']?.allowedHosts ?? []; + const allowedHosts = useMemo( + () => uiSchema?.['ui:options']?.allowedHosts ?? [], + [uiSchema], + ); const allowedOwners = useMemo( () => uiSchema?.['ui:options']?.allowedOwners ?? [], [uiSchema], @@ -65,6 +66,9 @@ export const RepoUrlPicker = ({ [setState], ); + const hostType = + (state.host && integrationApi.byHost(state.host)?.type) ?? null; + return ( <> setState(prevState => ({ ...prevState, host }))} rawErrors={rawErrors} /> - {state.host && integrationApi.byHost(state.host)?.type === 'github' && ( + {hostType === 'github' && ( )} - {state.host && integrationApi.byHost(state.host)?.type === 'gitlab' && ( + {hostType === 'gitlab' && ( )} - {state.host && - integrationApi.byHost(state.host)?.type === 'bitbucket' && ( - - )} - {state.host && integrationApi.byHost(state.host)?.type === 'azure' && ( + {hostType === 'bitbucket' && ( + + )} + {hostType === 'azure' && ( void; rawErrors: string[]; }) => { + const { host, hosts, onChange, rawErrors } = props; const scaffolderApi = useApi(scaffolderApiRef); const { value: integrations, loading } = useAsync(async () => { From ab81d5766b6695e32574adc7a589ee07f083aa5c Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 18 Jan 2022 08:21:16 +0100 Subject: [PATCH 22/22] chore: updating props api-report Signed-off-by: blam --- plugins/scaffolder/api-report.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index bc9eeb914b..8443de563e 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -147,12 +147,9 @@ 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: ({ - uiSchema, - onChange, - rawErrors, - formData, -}: FieldExtensionComponentProps) => JSX.Element; +export const RepoUrlPicker: ( + props: FieldExtensionComponentProps, +) => 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) //