From df955de8151075a32e5280bf3b137f160ca1edc0 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Jun 2024 14:12:38 +0200 Subject: [PATCH 01/24] feat: add listBranchesByRepository method to BitbucketCloudClient Signed-off-by: Benjamin Janssens --- .../src/BitbucketCloudClient.ts | 20 +++++++++++++++++++ .../src/models/index.ts | 11 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts index f1b7d60e41..86f56dd70d 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts @@ -95,6 +95,26 @@ export class BitbucketCloudClient { ); } + listBranchesByRepository( + repository: string, + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination { + const workspaceEnc = encodeURIComponent(workspace); + + return new WithPagination( + paginationOptions => + this.createUrl( + `/repositories/${workspaceEnc}/${repository}/refs/branches`, + { + ...paginationOptions, + ...options, + }, + ), + url => this.getTypeMapped(url), + ); + } + private createUrl(endpoint: string, options?: RequestOptions): URL { const request = new URL(this.config.apiBaseUrl + endpoint); for (const key in options) { diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index 2cdc143d06..28b1b45e1b 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -275,6 +275,17 @@ export namespace Models { values?: Set; } + /** + * A paginated list of branches. + * @public + */ + export interface PaginatedBranches extends Paginated { + /** + * The values of the current page. + */ + values?: Set; + } + /** * Object describing a user's role on resources like commits or pull requests. * @public From 7ee5682ad467090c3cb99866968a1efddfef1eb9 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Jun 2024 14:18:07 +0200 Subject: [PATCH 02/24] feat: add branches resource to autocomplete endpoint for bitbucket Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 31a2103088..70824995ac 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -77,6 +77,23 @@ export async function handleAutocompleteRequest({ return { results: result.map(title => ({ title })) }; } + case 'branches': { + if (!parameters.workspace || !parameters.repository) + throw new InputError( + 'Missing workspace and/or repository query parameter', + ); + + const result: string[] = []; + + for await (const page of client + .listBranchesByRepository(parameters.repository, parameters.workspace) + .iteratePages()) { + const names = [...page.values!].map(p => p.name!); + result.push(...names); + } + + return result; + } default: throw new InputError(`Invalid resource: ${resource}`); } From f6fea341737fd8e1820d882b5c4eeb2c2d4abb25 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 17 Jun 2024 16:32:15 +0200 Subject: [PATCH 03/24] feat: add BitbucketRepoBranchPicker Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 145 ++++++++++++++++++ .../fields/BitbucketRepoBranchPicker/index.ts | 20 +++ .../BitbucketRepoBranchPicker/schema.ts | 82 ++++++++++ plugins/scaffolder/src/extensions/default.ts | 7 + plugins/scaffolder/src/index.ts | 1 + plugins/scaffolder/src/plugin.tsx | 15 ++ 6 files changed, 270 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx new file mode 100644 index 0000000000..df496516af --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2024 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 { + scaffolderApiRef, + useTemplateSecrets, +} from '@backstage/plugin-scaffolder-react'; +import FormControl from '@material-ui/core/FormControl'; +import React, { useEffect, useMemo, useState } from 'react'; +import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import useDebounce from 'react-use/esm/useDebounce'; +import { useApi } from '@backstage/core-plugin-api'; +import { scmAuthApiRef } from '@backstage/integration-react'; +import { BitbucketRepoBranchPickerProps } from './schema'; + +export const BitbucketRepoBranchPicker = ({ + onChange, + rawErrors, + required, + formData, + formContext: { + formData: { repoUrl }, + }, + uiSchema, +}: BitbucketRepoBranchPickerProps) => { + const { secrets, setSecrets } = useTemplateSecrets(); + + const accessToken = useMemo( + () => + uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey && + secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey], + [secrets, uiSchema], + ); + + const [host, setHost] = useState(); + const [workspace, setWorkspace] = useState(null); + const [repository, setRepository] = useState(null); + const [availableBranches, setAvailableBranches] = useState([]); + + const scmAuthApi = useApi(scmAuthApiRef); + const scaffolderApi = useApi(scaffolderApiRef); + + useEffect(() => { + if (repoUrl) { + const url = new URL(`https://${repoUrl}`); + setHost(url.host); + setWorkspace(url.searchParams.get('workspace')); + setRepository(url.searchParams.get('repo')); + } + }, [repoUrl]); + + useDebounce( + async () => { + const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; + + if (!requestUserCredentials || !host) { + return; + } + + // don't show login prompt if secret value is already in state + if (secrets[requestUserCredentials.secretsKey]) { + return; + } + + // user has requested that we use the users credentials + // so lets grab them using the scmAuthApi and pass through + // any additional scopes from the ui:options + const { token } = await scmAuthApi.getCredentials({ + url: `https://${host}`, + additionalScope: { + repoWrite: true, + customScopes: requestUserCredentials.additionalScopes, + }, + }); + + // set the secret using the key provided in the ui:options for use + // in the templating the manifest with ${{ secrets[secretsKey] }} + setSecrets({ [requestUserCredentials.secretsKey]: token }); + }, + 500, + [host, uiSchema], + ); + + useDebounce( + () => { + const updateAvailableBranches = async () => { + if ( + host === 'bitbucket.org' && + accessToken && + workspace && + repository + ) { + const result = await scaffolderApi.autocomplete( + accessToken, + 'bitbucketCloud', + 'branches', + { workspace, repository }, + ); + + setAvailableBranches(result); + } else { + setAvailableBranches([]); + } + }; + + updateAvailableBranches().catch(() => setAvailableBranches([])); + }, + 500, + [host, accessToken, workspace, repository], + ); + + return ( + 0 && !formData} + > + { + onChange(newValue || ''); + }} + options={availableBranches} + renderInput={params => ( + + )} + freeSolo + autoSelect + /> + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts new file mode 100644 index 0000000000..df9acbd8b9 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 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 { + BitbucketRepoBranchPickerFieldSchema, + type BitbucketRepoBranchPickerUiOptions, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts new file mode 100644 index 0000000000..3e14888ed5 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2024 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 { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +/** + * @public + */ +export const BitbucketRepoBranchPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + requestUserCredentials: z + .object({ + secretsKey: z + .string() + .describe( + 'Key used within the template secrets context to store the credential', + ), + additionalScopes: z + .object({ + gitea: z + .array(z.string()) + .optional() + .describe('Additional Gitea scopes to request'), + gerrit: z + .array(z.string()) + .optional() + .describe('Additional Gerrit scopes to request'), + github: z + .array(z.string()) + .optional() + .describe('Additional GitHub scopes to request'), + gitlab: z + .array(z.string()) + .optional() + .describe('Additional GitLab scopes to request'), + bitbucket: z + .array(z.string()) + .optional() + .describe('Additional BitBucket scopes to request'), + azure: z + .array(z.string()) + .optional() + .describe('Additional Azure scopes to request'), + }) + .optional() + .describe('Additional permission scopes to request'), + }) + .optional() + .describe( + 'If defined will request user credentials to auth against the given SCM platform', + ), + }), +); + +/** + * The input props that can be specified under `ui:options` for the + * `BitbucketRepoBranchPicker` field extension. + * + * @public + */ +export type BitbucketRepoBranchPickerUiOptions = + typeof BitbucketRepoBranchPickerFieldSchema.uiOptionsType; + +export type BitbucketRepoBranchPickerProps = + typeof BitbucketRepoBranchPickerFieldSchema.type; + +export const BitbucketRepoBranchPickerSchema = + BitbucketRepoBranchPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index a59f3e08e3..13a719e9f9 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -50,6 +50,8 @@ import { MultiEntityPickerSchema, validateMultiEntityPickerValidation, } from '../components/fields/MultiEntityPicker/MultiEntityPicker'; +import { BitbucketRepoBranchPicker } from '../components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker'; +import { BitbucketRepoBranchPickerSchema } from '../components/fields/BitbucketRepoBranchPicker/schema'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -99,4 +101,9 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ schema: MultiEntityPickerSchema, validation: validateMultiEntityPickerValidation, }, + { + component: BitbucketRepoBranchPicker, + name: 'BitbucketRepoBranchPicker', + schema: BitbucketRepoBranchPickerSchema, + }, ]; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index d809ec00f9..d343311db9 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -30,6 +30,7 @@ export { MyGroupsPickerFieldExtension, RepoUrlPickerFieldExtension, MultiEntityPickerFieldExtension, + BitbucketRepoBranchPickerFieldExtension, ScaffolderPage, scaffolderPlugin, } from './plugin'; diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index c053bfacd9..453e5ea63a 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -73,6 +73,8 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from './components/fields/MyGroupsPicker/MyGroupsPicker'; +import { BitbucketRepoBranchPicker } from './components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker'; +import { BitbucketRepoBranchPickerSchema } from './components/fields/BitbucketRepoBranchPicker/schema'; /** * The main plugin export for the scaffolder. @@ -231,3 +233,16 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( schema: EntityTagsPickerSchema, }), ); + +/** + * A field extension to select a branch from a repository. + * + * @public + */ +export const BitbucketRepoBranchPickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + component: BitbucketRepoBranchPicker, + name: 'BitbucketRepoBranchPicker', + schema: BitbucketRepoBranchPickerSchema, + }), +); From 741837a560f16369b3aadce4294e02726e5732e8 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 11:42:20 +0200 Subject: [PATCH 04/24] refactor: add generic RepoBranchPicker Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 109 ++++----------- .../RepoBranchPicker.tsx | 125 ++++++++++++++++++ .../fields/BitbucketRepoBranchPicker/index.ts | 4 +- .../BitbucketRepoBranchPicker/schema.ts | 14 +- .../fields/BitbucketRepoBranchPicker/types.ts | 23 ++++ plugins/scaffolder/src/extensions/default.ts | 10 +- plugins/scaffolder/src/index.ts | 2 +- plugins/scaffolder/src/plugin.tsx | 12 +- 8 files changed, 194 insertions(+), 105 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx index df496516af..736841b7a6 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -14,128 +14,71 @@ * limitations under the License. */ -import { - scaffolderApiRef, - useTemplateSecrets, -} from '@backstage/plugin-scaffolder-react'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import FormControl from '@material-ui/core/FormControl'; -import React, { useEffect, useMemo, useState } from 'react'; +import React from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; import { useApi } from '@backstage/core-plugin-api'; -import { scmAuthApiRef } from '@backstage/integration-react'; -import { BitbucketRepoBranchPickerProps } from './schema'; +import { RepoBranchPickerState } from './types'; export const BitbucketRepoBranchPicker = ({ onChange, + state, rawErrors, - required, - formData, - formContext: { - formData: { repoUrl }, - }, - uiSchema, -}: BitbucketRepoBranchPickerProps) => { - const { secrets, setSecrets } = useTemplateSecrets(); - - const accessToken = useMemo( - () => - uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey && - secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey], - [secrets, uiSchema], - ); - - const [host, setHost] = useState(); - const [workspace, setWorkspace] = useState(null); - const [repository, setRepository] = useState(null); - const [availableBranches, setAvailableBranches] = useState([]); - - const scmAuthApi = useApi(scmAuthApiRef); + accessToken, +}: { + onChange: (state: RepoBranchPickerState) => void; + state: RepoBranchPickerState; + rawErrors: string[]; + accessToken?: string; +}) => { const scaffolderApi = useApi(scaffolderApiRef); - useEffect(() => { - if (repoUrl) { - const url = new URL(`https://${repoUrl}`); - setHost(url.host); - setWorkspace(url.searchParams.get('workspace')); - setRepository(url.searchParams.get('repo')); - } - }, [repoUrl]); - - useDebounce( - async () => { - const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; - - if (!requestUserCredentials || !host) { - return; - } - - // don't show login prompt if secret value is already in state - if (secrets[requestUserCredentials.secretsKey]) { - return; - } - - // user has requested that we use the users credentials - // so lets grab them using the scmAuthApi and pass through - // any additional scopes from the ui:options - const { token } = await scmAuthApi.getCredentials({ - url: `https://${host}`, - additionalScope: { - repoWrite: true, - customScopes: requestUserCredentials.additionalScopes, - }, - }); - - // set the secret using the key provided in the ui:options for use - // in the templating the manifest with ${{ secrets[secretsKey] }} - setSecrets({ [requestUserCredentials.secretsKey]: token }); - }, - 500, - [host, uiSchema], - ); - useDebounce( () => { const updateAvailableBranches = async () => { if ( - host === 'bitbucket.org' && + state.host === 'bitbucket.org' && accessToken && - workspace && - repository + state.workspace && + state.repository ) { const result = await scaffolderApi.autocomplete( accessToken, 'bitbucketCloud', 'branches', - { workspace, repository }, + { workspace: state.workspace, repository: state.repository }, ); - setAvailableBranches(result); + onChange({ availableBranches: result }); } else { - setAvailableBranches([]); + onChange({ availableBranches: [] }); } }; - updateAvailableBranches().catch(() => setAvailableBranches([])); + updateAvailableBranches().catch(() => + onChange({ availableBranches: [] }), + ); }, 500, - [host, accessToken, workspace, repository], + [state, accessToken], ); return ( 0 && !formData} + required + error={rawErrors?.length > 0 && !state.branch} > { - onChange(newValue || ''); + onChange({ branch: newValue || undefined }); }} - options={availableBranches} + options={state.availableBranches || []} renderInput={params => ( - + )} freeSolo autoSelect diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx new file mode 100644 index 0000000000..1f75090e3b --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx @@ -0,0 +1,125 @@ +/* + * Copyright 2024 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, + scmAuthApiRef, +} from '@backstage/integration-react'; +import React, { useEffect, useState, useCallback } from 'react'; +import { RepoBranchPickerProps } from './schema'; +import { RepoBranchPickerState } from './types'; +import useDebounce from 'react-use/esm/useDebounce'; +import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react'; +import Box from '@material-ui/core/Box'; +import Divider from '@material-ui/core/Divider'; +import Typography from '@material-ui/core/Typography'; +import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; + +/** + * The underlying component that is rendered in the form for the `RepoBranchPicker` + * field extension. + * + * @public + */ +export const RepoBranchPicker = (props: RepoBranchPickerProps) => { + const { uiSchema, onChange, rawErrors, schema, formContext } = props; + const [state, setState] = useState({}); + const integrationApi = useApi(scmIntegrationsApiRef); + const scmAuthApi = useApi(scmAuthApiRef); + const { secrets, setSecrets } = useTemplateSecrets(); + + useEffect(() => { + if (formContext.formData.repoUrl) { + const url = new URL(`https://${formContext.formData.repoUrl}`); + + setState({ + host: url.host, + workspace: url.searchParams.get('workspace') || undefined, + repository: url.searchParams.get('repo') || undefined, + }); + } + }, [formContext]); + + useEffect(() => { + onChange(state.branch); + }, [state, onChange]); + + const updateLocalState = useCallback( + (newState: RepoBranchPickerState) => { + setState(prevState => ({ ...prevState, ...newState })); + }, + [setState], + ); + + useDebounce( + async () => { + const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; + + if (!requestUserCredentials || !state.host) { + return; + } + + // don't show login prompt if secret value is already in state + if (secrets[requestUserCredentials.secretsKey]) { + return; + } + + // user has requested that we use the users credentials + // so lets grab them using the scmAuthApi and pass through + // any additional scopes from the ui:options + const { token } = await scmAuthApi.getCredentials({ + url: `https://${state.host}`, + additionalScope: { + repoWrite: true, + customScopes: requestUserCredentials.additionalScopes, + }, + }); + + // set the secret using the key provided in the ui:options for use + // in the templating the manifest with ${{ secrets[secretsKey] }} + setSecrets({ [requestUserCredentials.secretsKey]: token }); + }, + 500, + [state, uiSchema], + ); + + const hostType = + (state.host && integrationApi.byHost(state.host)?.type) ?? null; + return ( + <> + {schema.title && ( + + {schema.title} + + + )} + {schema.description && ( + {schema.description} + )} + {hostType === 'bitbucket' && ( + + )} + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts index df9acbd8b9..2b87b62ebd 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts @@ -15,6 +15,6 @@ */ export { - BitbucketRepoBranchPickerFieldSchema, - type BitbucketRepoBranchPickerUiOptions, + RepoBranchPickerFieldSchema, + type RepoBranchPickerUiOptions, } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts index 3e14888ed5..12ed07fd80 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts @@ -19,7 +19,7 @@ import { makeFieldSchemaFromZod } from '../utils'; /** * @public */ -export const BitbucketRepoBranchPickerFieldSchema = makeFieldSchemaFromZod( +export const RepoBranchPickerFieldSchema = makeFieldSchemaFromZod( z.string(), z.object({ requestUserCredentials: z @@ -68,15 +68,13 @@ export const BitbucketRepoBranchPickerFieldSchema = makeFieldSchemaFromZod( /** * The input props that can be specified under `ui:options` for the - * `BitbucketRepoBranchPicker` field extension. + * `RepoBranchPicker` field extension. * * @public */ -export type BitbucketRepoBranchPickerUiOptions = - typeof BitbucketRepoBranchPickerFieldSchema.uiOptionsType; +export type RepoBranchPickerUiOptions = + typeof RepoBranchPickerFieldSchema.uiOptionsType; -export type BitbucketRepoBranchPickerProps = - typeof BitbucketRepoBranchPickerFieldSchema.type; +export type RepoBranchPickerProps = typeof RepoBranchPickerFieldSchema.type; -export const BitbucketRepoBranchPickerSchema = - BitbucketRepoBranchPickerFieldSchema.schema; +export const RepoBranchPickerSchema = RepoBranchPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts new file mode 100644 index 0000000000..4843ff767e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2024 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 RepoBranchPickerState { + host?: string; + workspace?: string; + repository?: string; + branch?: string; + availableBranches?: string[]; +} diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 13a719e9f9..409372e421 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -50,8 +50,8 @@ import { MultiEntityPickerSchema, validateMultiEntityPickerValidation, } from '../components/fields/MultiEntityPicker/MultiEntityPicker'; -import { BitbucketRepoBranchPicker } from '../components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker'; -import { BitbucketRepoBranchPickerSchema } from '../components/fields/BitbucketRepoBranchPicker/schema'; +import { RepoBranchPicker } from '../components/fields/BitbucketRepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from '../components/fields/BitbucketRepoBranchPicker/schema'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -102,8 +102,8 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ validation: validateMultiEntityPickerValidation, }, { - component: BitbucketRepoBranchPicker, - name: 'BitbucketRepoBranchPicker', - schema: BitbucketRepoBranchPickerSchema, + component: RepoBranchPicker, + name: 'RepoBranchPicker', + schema: RepoBranchPickerSchema, }, ]; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index d343311db9..c5709f1080 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -30,7 +30,7 @@ export { MyGroupsPickerFieldExtension, RepoUrlPickerFieldExtension, MultiEntityPickerFieldExtension, - BitbucketRepoBranchPickerFieldExtension, + RepoBranchPickerFieldExtension, ScaffolderPage, scaffolderPlugin, } from './plugin'; diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index 453e5ea63a..6f4e28c9b0 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -73,8 +73,8 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from './components/fields/MyGroupsPicker/MyGroupsPicker'; -import { BitbucketRepoBranchPicker } from './components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker'; -import { BitbucketRepoBranchPickerSchema } from './components/fields/BitbucketRepoBranchPicker/schema'; +import { RepoBranchPicker } from './components/fields/BitbucketRepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from './components/fields/BitbucketRepoBranchPicker/schema'; /** * The main plugin export for the scaffolder. @@ -239,10 +239,10 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( * * @public */ -export const BitbucketRepoBranchPickerFieldExtension = scaffolderPlugin.provide( +export const RepoBranchPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ - component: BitbucketRepoBranchPicker, - name: 'BitbucketRepoBranchPicker', - schema: BitbucketRepoBranchPickerSchema, + component: RepoBranchPicker, + name: 'RepoBranchPicker', + schema: RepoBranchPickerSchema, }), ); From 3a272ac051baf5be2417be598a591859f4ef30b3 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 11:54:00 +0200 Subject: [PATCH 05/24] chore: code cleanup Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 14 ++--- .../RepoBranchPicker.tsx | 54 ++++++++++--------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx index 736841b7a6..70f4ea8c36 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -34,22 +34,24 @@ export const BitbucketRepoBranchPicker = ({ rawErrors: string[]; accessToken?: string; }) => { + const { host, workspace, repository, branch, availableBranches } = state; + const scaffolderApi = useApi(scaffolderApiRef); useDebounce( () => { const updateAvailableBranches = async () => { if ( - state.host === 'bitbucket.org' && + host === 'bitbucket.org' && accessToken && - state.workspace && - state.repository + workspace && + repository ) { const result = await scaffolderApi.autocomplete( accessToken, 'bitbucketCloud', 'branches', - { workspace: state.workspace, repository: state.repository }, + { workspace, repository }, ); onChange({ availableBranches: result }); @@ -70,13 +72,13 @@ export const BitbucketRepoBranchPicker = ({ 0 && !state.branch} + error={rawErrors?.length > 0 && !branch} > { onChange({ branch: newValue || undefined }); }} - options={state.availableBranches || []} + options={availableBranches || []} renderInput={params => ( )} diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx index 1f75090e3b..c690a58f4a 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx @@ -36,34 +36,15 @@ import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; */ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { const { uiSchema, onChange, rawErrors, schema, formContext } = props; + const [state, setState] = useState({}); + const { host, branch } = state; + const integrationApi = useApi(scmIntegrationsApiRef); const scmAuthApi = useApi(scmAuthApiRef); + const { secrets, setSecrets } = useTemplateSecrets(); - useEffect(() => { - if (formContext.formData.repoUrl) { - const url = new URL(`https://${formContext.formData.repoUrl}`); - - setState({ - host: url.host, - workspace: url.searchParams.get('workspace') || undefined, - repository: url.searchParams.get('repo') || undefined, - }); - } - }, [formContext]); - - useEffect(() => { - onChange(state.branch); - }, [state, onChange]); - - const updateLocalState = useCallback( - (newState: RepoBranchPickerState) => { - setState(prevState => ({ ...prevState, ...newState })); - }, - [setState], - ); - useDebounce( async () => { const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; @@ -96,8 +77,31 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { [state, uiSchema], ); - const hostType = - (state.host && integrationApi.byHost(state.host)?.type) ?? null; + useEffect(() => { + if (formContext.formData.repoUrl) { + const url = new URL(`https://${formContext.formData.repoUrl}`); + + setState({ + host: url.host, + workspace: url.searchParams.get('workspace') || undefined, + repository: url.searchParams.get('repo') || undefined, + }); + } + }, [formContext]); + + useEffect(() => { + onChange(branch); + }, [branch, onChange]); + + const updateLocalState = useCallback( + (newState: RepoBranchPickerState) => { + setState(prevState => ({ ...prevState, ...newState })); + }, + [setState], + ); + + const hostType = (host && integrationApi.byHost(host)?.type) ?? null; + return ( <> {schema.title && ( From 4e3ba7ca74ed3e675e607ceb88d0c1759b65442d Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 13:37:38 +0200 Subject: [PATCH 06/24] chore: code cleanup Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 7 ++-- .../RepoBranchPicker.tsx | 32 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx index 70f4ea8c36..27a0b5c027 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -22,6 +22,7 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; import { useApi } from '@backstage/core-plugin-api'; import { RepoBranchPickerState } from './types'; +import FormHelperText from '@material-ui/core/FormHelperText'; export const BitbucketRepoBranchPicker = ({ onChange, @@ -65,7 +66,7 @@ export const BitbucketRepoBranchPicker = ({ ); }, 500, - [state, accessToken], + [host, workspace, repository, accessToken], ); return ( @@ -75,8 +76,9 @@ export const BitbucketRepoBranchPicker = ({ error={rawErrors?.length > 0 && !branch} > { - onChange({ branch: newValue || undefined }); + onChange({ branch: newValue || '' }); }} options={availableBranches || []} renderInput={params => ( @@ -85,6 +87,7 @@ export const BitbucketRepoBranchPicker = ({ freeSolo autoSelect /> + The branch of the repository ); }; diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx index c690a58f4a..2ba1eddbfc 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { useApi } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef, @@ -35,9 +36,15 @@ import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; * @public */ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { - const { uiSchema, onChange, rawErrors, schema, formContext } = props; + const { uiSchema, onChange, rawErrors, formData, schema, formContext } = + props; + const { + formData: { repoUrl }, + } = formContext; - const [state, setState] = useState({}); + const [state, setState] = useState({ + branch: formData || '', + }); const { host, branch } = state; const integrationApi = useApi(scmIntegrationsApiRef); @@ -49,7 +56,7 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { async () => { const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; - if (!requestUserCredentials || !state.host) { + if (!requestUserCredentials || !host) { return; } @@ -62,7 +69,7 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { // so lets grab them using the scmAuthApi and pass through // any additional scopes from the ui:options const { token } = await scmAuthApi.getCredentials({ - url: `https://${state.host}`, + url: `https://${host}`, additionalScope: { repoWrite: true, customScopes: requestUserCredentials.additionalScopes, @@ -74,20 +81,21 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { setSecrets({ [requestUserCredentials.secretsKey]: token }); }, 500, - [state, uiSchema], + [host, uiSchema], ); useEffect(() => { - if (formContext.formData.repoUrl) { - const url = new URL(`https://${formContext.formData.repoUrl}`); + if (repoUrl) { + const url = new URL(`https://${repoUrl}`); - setState({ + setState(prevState => ({ + ...prevState, host: url.host, - workspace: url.searchParams.get('workspace') || undefined, - repository: url.searchParams.get('repo') || undefined, - }); + workspace: url.searchParams.get('workspace') || '', + repository: url.searchParams.get('repo') || '', + })); } - }, [formContext]); + }, [repoUrl]); useEffect(() => { onChange(branch); From fb528a9196450b09667950fa293108ce7f83dca4 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 14:44:12 +0200 Subject: [PATCH 07/24] refactor: move availableBranches from RepoBranchPickerState to BitbucketRepoBranchPicker; add tests for BitbucketRepoBranchPicker Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.test.tsx | 105 ++++++++++++++++++ .../BitbucketRepoBranchPicker.tsx | 16 +-- .../fields/BitbucketRepoBranchPicker/types.ts | 1 - 3 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx new file mode 100644 index 0000000000..6cbc561bc9 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2024 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 { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; +import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; + +describe('BitbucketRepoBranchPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest.fn().mockResolvedValue(['branch1']), + }; + + it('renders an input field', () => { + const { getByRole } = render( + + + , + ); + + expect(getByRole('textbox')).toBeInTheDocument(); + expect(getByRole('textbox')).toHaveValue('main'); + }); + + it('calls onChange when the input field changes', () => { + const onChange = jest.fn(); + + const { getByRole } = render( + + + , + ); + + const input = getByRole('textbox'); + + act(() => { + input.focus(); + fireEvent.change(input, { + target: { value: 'develop' }, + }); + input.blur(); + }); + + expect(onChange).toHaveBeenCalledWith({ branch: 'develop' }); + }); + + it('should populate branches', async () => { + const onChange = jest.fn(); + + const { getByRole, getByText } = render( + + + , + ); + + // Open the Autcomplete dropdown + const input = getByRole('textbox'); + await userEvent.click(input); + + // Verify that the available workspaces are shown + await waitFor(() => expect(getByText('branch1')).toBeInTheDocument()); + + // Verify that selecting an option calls onChange + await userEvent.click(getByText('branch1')); + expect(onChange).toHaveBeenCalledWith({ + branch: 'branch1', + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx index 27a0b5c027..3af7eb7e2c 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -16,7 +16,7 @@ import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import FormControl from '@material-ui/core/FormControl'; -import React from 'react'; +import React, { useState } from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; @@ -35,7 +35,9 @@ export const BitbucketRepoBranchPicker = ({ rawErrors: string[]; accessToken?: string; }) => { - const { host, workspace, repository, branch, availableBranches } = state; + const { host, workspace, repository, branch } = state; + + const [availableBranches, setAvailableBranches] = useState([]); const scaffolderApi = useApi(scaffolderApiRef); @@ -55,15 +57,13 @@ export const BitbucketRepoBranchPicker = ({ { workspace, repository }, ); - onChange({ availableBranches: result }); + setAvailableBranches(result); } else { - onChange({ availableBranches: [] }); + setAvailableBranches([]); } }; - updateAvailableBranches().catch(() => - onChange({ availableBranches: [] }), - ); + updateAvailableBranches().catch(() => setAvailableBranches([])); }, 500, [host, workspace, repository, accessToken], @@ -80,7 +80,7 @@ export const BitbucketRepoBranchPicker = ({ onChange={(_, newValue) => { onChange({ branch: newValue || '' }); }} - options={availableBranches || []} + options={availableBranches} renderInput={params => ( )} diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts index 4843ff767e..1d54ea7e8b 100644 --- a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts @@ -19,5 +19,4 @@ export interface RepoBranchPickerState { workspace?: string; repository?: string; branch?: string; - availableBranches?: string[]; } From a781cd8513b9ec3a86bcb2379be7fbc5b5a49d2c Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 14:46:20 +0200 Subject: [PATCH 08/24] chore: rename BitbucketRepoBranchPicker folder to RepoBranchPicker Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.test.tsx | 0 .../BitbucketRepoBranchPicker.tsx | 0 .../RepoBranchPicker.tsx | 0 .../{BitbucketRepoBranchPicker => RepoBranchPicker}/index.ts | 0 .../{BitbucketRepoBranchPicker => RepoBranchPicker}/schema.ts | 0 .../{BitbucketRepoBranchPicker => RepoBranchPicker}/types.ts | 0 plugins/scaffolder/src/extensions/default.ts | 4 ++-- plugins/scaffolder/src/plugin.tsx | 4 ++-- 8 files changed, 4 insertions(+), 4 deletions(-) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/BitbucketRepoBranchPicker.test.tsx (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/BitbucketRepoBranchPicker.tsx (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/RepoBranchPicker.tsx (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/index.ts (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/schema.ts (100%) rename plugins/scaffolder/src/components/fields/{BitbucketRepoBranchPicker => RepoBranchPicker}/types.ts (100%) diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.test.tsx rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/BitbucketRepoBranchPicker.tsx rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/RepoBranchPicker.tsx rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/index.ts rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/schema.ts rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts diff --git a/plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts similarity index 100% rename from plugins/scaffolder/src/components/fields/BitbucketRepoBranchPicker/types.ts rename to plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 409372e421..e77d968c80 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -50,8 +50,8 @@ import { MultiEntityPickerSchema, validateMultiEntityPickerValidation, } from '../components/fields/MultiEntityPicker/MultiEntityPicker'; -import { RepoBranchPicker } from '../components/fields/BitbucketRepoBranchPicker/RepoBranchPicker'; -import { RepoBranchPickerSchema } from '../components/fields/BitbucketRepoBranchPicker/schema'; +import { RepoBranchPicker } from '../components/fields/RepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from '../components/fields/RepoBranchPicker/schema'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index 6f4e28c9b0..c6c858f6b1 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -73,8 +73,8 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from './components/fields/MyGroupsPicker/MyGroupsPicker'; -import { RepoBranchPicker } from './components/fields/BitbucketRepoBranchPicker/RepoBranchPicker'; -import { RepoBranchPickerSchema } from './components/fields/BitbucketRepoBranchPicker/schema'; +import { RepoBranchPicker } from './components/fields/RepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from './components/fields/RepoBranchPicker/schema'; /** * The main plugin export for the scaffolder. From 426bd26366c0f649a36a89f70acd0e97e9da428b Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 15:00:14 +0200 Subject: [PATCH 09/24] test: add tests for listBranchesByRepository to BitbucketCloudClient Signed-off-by: Benjamin Janssens --- .../src/BitbucketCloudClient.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts index f625f0daf8..7558c92138 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts @@ -162,4 +162,33 @@ describe('BitbucketCloudClient', () => { expect(results).toHaveLength(1); expect(results[0].slug).toEqual('workspace1'); }); + + it('listBranchesByRepository', async () => { + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace1/repo1/refs/branches', + (_, res, ctx) => { + const response = { + values: [ + { + type: 'branch', + name: 'branch1', + } as Models.Branch, + ], + }; + return res(ctx.json(response)); + }, + ), + ); + + const pagination = client.listBranchesByRepository('repo1', 'workspace1'); + + const results = []; + for await (const result of pagination.iterateResults()) { + results.push(result); + } + + expect(results).toHaveLength(1); + expect(results[0].name).toEqual('branch1'); + }); }); From 5588a64e80e324ab97cb9647b477cee2793cff49 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 15:09:46 +0200 Subject: [PATCH 10/24] test: add tests for branches for handleBitbucketCloudRequest Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts index 0447ff2d28..b124c65f78 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts @@ -35,6 +35,11 @@ describe('handleAutocompleteRequest', () => { .fn() .mockReturnValue([{ values: [{ slug: 'repository1' }] }]), }), + listBranchesByRepository: jest.fn().mockReturnValue({ + iteratePages: jest + .fn() + .mockReturnValue([{ values: [{ name: 'branch1' }] }]), + }), }; const fromConfig = jest @@ -89,6 +94,15 @@ describe('handleAutocompleteRequest', () => { expect(result).toEqual({ results: [{ title: 'repository1' }] }); }); + it('should return branches', async () => { + const result = await handleBitbucketCloudRequest('foo', 'branches', { + workspace: 'workspace1', + repository: 'repository1', + }); + + expect(result).toEqual(['branch1']); + }); + it('should throw an error when passing an invalid resource', async () => { await expect( handleAutocompleteRequest({ From 8329e930c6f7e7f0b3654b2e7af8d098d246527a Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 19 Jun 2024 09:52:17 +0200 Subject: [PATCH 11/24] test: add tests for RepoBranchPicker Signed-off-by: Benjamin Janssens --- .../RepoBranchPicker.test.tsx | 324 ++++++++++++++++++ .../RepoBranchPicker/RepoBranchPicker.tsx | 5 +- 2 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx new file mode 100644 index 0000000000..eb5ebd1730 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx @@ -0,0 +1,324 @@ +/* + * Copyright 2024 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 { Form } from '@backstage/plugin-scaffolder-react/alpha'; +import validator from '@rjsf/validator-ajv8'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + scmIntegrationsApiRef, + ScmIntegrationsApi, + scmAuthApiRef, + ScmAuthApi, +} from '@backstage/integration-react'; + +import { + SecretsContextProvider, + scaffolderApiRef, + useTemplateSecrets, + ScaffolderRJSFField, +} from '@backstage/plugin-scaffolder-react'; +import { act, fireEvent } from '@testing-library/react'; +import { RepoBranchPicker } from './RepoBranchPicker'; + +describe('RepoBranchPicker', () => { + const mockIntegrationsApi: Partial = { + byHost: () => ({ type: 'bitbucket' }), + }; + + let mockScmAuthApi: Partial; + + beforeEach(() => { + mockScmAuthApi = { + getCredentials: jest.fn().mockResolvedValue({ token: 'abc123' }), + }; + }); + + describe('happy path rendering', () => { + it('should render the repo branch picker with minimal props', async () => { + const onSubmit = jest.fn(); + + const { getByRole } = await renderInTestApp( + + +
, + }} + onSubmit={onSubmit} + formContext={{ + formData: { + repoUrl: 'bitbucket.org', + }, + }} + /> + + , + ); + + const input = getByRole('textbox'); + const submitButton = getByRole('button'); + + act(() => { + input.focus(); + fireEvent.change(input, { target: { value: 'branch1' } }); + input.blur(); + }); + + fireEvent.click(submitButton); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + formData: 'branch1', + }), + expect.anything(), + ); + }); + + it('should render properly with title and description', async () => { + const { getByText } = await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'bitbucket.org', + }, + }} + /> + + , + ); + + expect(getByText('test title')).toBeInTheDocument(); + expect(getByText('test description')).toBeInTheDocument(); + }); + }); + + describe('requestUserCredentials', () => { + it('should call the scmAuthApi with the correct params', async () => { + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + return ( +
{JSON.stringify({ secrets })}
+ ); + }; + const { getByTestId } = await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'github.com', + }, + }} + /> + + + , + ); + + await act(async () => { + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({ + url: 'https://github.com', + additionalScope: { + repoWrite: true, + customScopes: { + github: ['workflow'], + }, + }, + }); + + const currentSecrets = JSON.parse( + getByTestId('current-secrets').textContent!, + ); + + expect(currentSecrets).toEqual({ + secrets: { testKey: 'abc123' }, + }); + }); + + it('should call the scmAuthApi with the correct params if workspace is nested', async () => { + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + return ( +
{JSON.stringify({ secrets })}
+ ); + }; + await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'gitlab.example.com', + }, + }} + /> + + + , + ); + + await act(async () => { + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({ + url: 'https://gitlab.example.com', + additionalScope: { + repoWrite: true, + }, + }); + }); + + it('should not call the scmAuthApi if secret is available in the state', async () => { + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + return ( +
{JSON.stringify({ secrets })}
+ ); + }; + const { getByTestId } = await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'github.com', + }, + }} + /> + + + , + ); + + await act(async () => { + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + // as we already have a secret in the state, getCredentials should not be called again. + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledTimes(0); + + const currentSecrets = JSON.parse( + getByTestId('current-secrets').textContent!, + ); + + expect(currentSecrets).toEqual({ + secrets: { testKey: 'abc123' }, + }); + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx index 2ba1eddbfc..8ab83bd717 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx @@ -20,8 +20,6 @@ import { scmAuthApiRef, } from '@backstage/integration-react'; import React, { useEffect, useState, useCallback } from 'react'; -import { RepoBranchPickerProps } from './schema'; -import { RepoBranchPickerState } from './types'; import useDebounce from 'react-use/esm/useDebounce'; import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react'; import Box from '@material-ui/core/Box'; @@ -29,6 +27,9 @@ import Divider from '@material-ui/core/Divider'; import Typography from '@material-ui/core/Typography'; import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; +import { RepoBranchPickerProps } from './schema'; +import { RepoBranchPickerState } from './types'; + /** * The underlying component that is rendered in the form for the `RepoBranchPicker` * field extension. From 80738256c37e4ace961dbf31fe0e02b04371075d Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 19 Jun 2024 13:48:49 +0200 Subject: [PATCH 12/24] chore: copy comments from RepoUrlPicker to stay consistent Signed-off-by: Benjamin Janssens --- .../fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx | 7 +++++++ .../src/components/fields/RepoBranchPicker/schema.ts | 3 +++ 2 files changed, 10 insertions(+) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index 3af7eb7e2c..23e8f01617 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -24,6 +24,13 @@ import { useApi } from '@backstage/core-plugin-api'; import { RepoBranchPickerState } from './types'; import FormHelperText from '@material-ui/core/FormHelperText'; +/** + * The underlying component that is rendered in the form for the `BitbucketRepoBranchPicker` + * field extension. + * + * @public + * + */ export const BitbucketRepoBranchPicker = ({ onChange, state, diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts index 12ed07fd80..6ee67bd1c7 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts @@ -77,4 +77,7 @@ export type RepoBranchPickerUiOptions = export type RepoBranchPickerProps = typeof RepoBranchPickerFieldSchema.type; +// NOTE: There is a bug with this failing validation in the custom field explorer due +// to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if +// requestUserCredentials is not defined export const RepoBranchPickerSchema = RepoBranchPickerFieldSchema.schema; From 37d52e4f2830ff15af55a60bfd5d8afd7ea36c2b Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 19 Jun 2024 16:37:33 +0200 Subject: [PATCH 13/24] feat: fall back on DefaultRepoBranchPicker if host is not bitbucket Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 6 +- .../DefaultRepoBranchPicker.test.tsx | 55 +++++++++++++++++ .../DefaultRepoBranchPicker.tsx | 60 +++++++++++++++++++ .../RepoBranchPicker.test.tsx | 10 +--- .../RepoBranchPicker/RepoBranchPicker.tsx | 53 +++++++++++----- 5 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index 23e8f01617..76de25dba0 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -36,11 +36,13 @@ export const BitbucketRepoBranchPicker = ({ state, rawErrors, accessToken, + required, }: { onChange: (state: RepoBranchPickerState) => void; state: RepoBranchPickerState; rawErrors: string[]; accessToken?: string; + required?: boolean; }) => { const { host, workspace, repository, branch } = state; @@ -79,7 +81,7 @@ export const BitbucketRepoBranchPicker = ({ return ( 0 && !branch} > ( - + )} freeSolo autoSelect diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx new file mode 100644 index 0000000000..77589e4478 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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 { fireEvent, render } from '@testing-library/react'; + +import { DefaultRepoBranchPicker } from './DefaultRepoBranchPicker'; + +describe('DefaultRepoBranchPicker', () => { + it('renders an input field', () => { + const { getByRole } = render( + , + ); + + expect(getByRole('textbox')).toBeInTheDocument(); + expect(getByRole('textbox')).toHaveValue('main'); + }); + + it('calls onChange when the input field changes', () => { + const onChange = jest.fn(); + + const { getByRole } = render( + , + ); + + const input = getByRole('textbox'); + + fireEvent.change(input, { + target: { value: 'develop' }, + }); + + expect(onChange).toHaveBeenCalledWith({ branch: 'develop' }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx new file mode 100644 index 0000000000..52a4a15e23 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2024 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 FormControl from '@material-ui/core/FormControl'; +import React from 'react'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; + +import { RepoBranchPickerState } from './types'; + +/** + * The underlying component that is rendered in the form for the `DefaultRepoBranchPicker` + * field extension. + * + * @public + * + */ +export const DefaultRepoBranchPicker = ({ + onChange, + state, + rawErrors, + required, +}: { + onChange: (state: RepoBranchPickerState) => void; + state: RepoBranchPickerState; + rawErrors: string[]; + required?: boolean; +}) => { + const { branch } = state; + + return ( + 0 && !branch} + > + Branch + onChange({ branch: e.target.value })} + value={branch} + /> + The branch of the repository + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx index eb5ebd1730..ece73670f6 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx @@ -70,9 +70,7 @@ describe('RepoBranchPicker', () => { }} onSubmit={onSubmit} formContext={{ - formData: { - repoUrl: 'bitbucket.org', - }, + formData: {}, }} /> @@ -82,11 +80,7 @@ describe('RepoBranchPicker', () => { const input = getByRole('textbox'); const submitButton = getByRole('button'); - act(() => { - input.focus(); - fireEvent.change(input, { target: { value: 'branch1' } }); - input.blur(); - }); + fireEvent.change(input, { target: { value: 'branch1' } }); fireEvent.click(submitButton); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx index 8ab83bd717..4ca22c8f80 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx @@ -25,10 +25,11 @@ import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react'; import Box from '@material-ui/core/Box'; import Divider from '@material-ui/core/Divider'; import Typography from '@material-ui/core/Typography'; -import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; import { RepoBranchPickerProps } from './schema'; import { RepoBranchPickerState } from './types'; +import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; +import { DefaultRepoBranchPicker } from './DefaultRepoBranchPicker'; /** * The underlying component that is rendered in the form for the `RepoBranchPicker` @@ -37,8 +38,15 @@ import { RepoBranchPickerState } from './types'; * @public */ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { - const { uiSchema, onChange, rawErrors, formData, schema, formContext } = - props; + const { + uiSchema, + onChange, + rawErrors, + formData, + schema, + formContext, + required, + } = props; const { formData: { repoUrl }, } = formContext; @@ -111,6 +119,33 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { const hostType = (host && integrationApi.byHost(host)?.type) ?? null; + const renderRepoBranchPicker = () => { + switch (hostType) { + case 'bitbucket': + return ( + + ); + default: + return ( + + ); + } + }; + return ( <> {schema.title && ( @@ -122,17 +157,7 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { {schema.description && ( {schema.description} )} - {hostType === 'bitbucket' && ( - - )} + {renderRepoBranchPicker()} ); }; From 7d82969f10086d0d062af813476df906b278e438 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 28 Jun 2024 11:56:51 +0200 Subject: [PATCH 14/24] chore: update BitbucketRepoBranchPicker and handleAutocompleteRequest Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.test.ts | 12 ++++++++---- .../src/autocomplete/autocomplete.ts | 8 ++++---- .../BitbucketRepoBranchPicker.test.tsx | 4 +++- .../BitbucketRepoBranchPicker.tsx | 17 +++++++++-------- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts index b124c65f78..d99a8358b9 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts @@ -95,12 +95,16 @@ describe('handleAutocompleteRequest', () => { }); it('should return branches', async () => { - const result = await handleBitbucketCloudRequest('foo', 'branches', { - workspace: 'workspace1', - repository: 'repository1', + const result = await handleAutocompleteRequest({ + token: 'foo', + resource: 'branches', + context: { + workspace: 'workspace1', + repository: 'repository1', + }, }); - expect(result).toEqual(['branch1']); + expect(result).toEqual({ results: [{ title: 'branch1' }] }); }); it('should throw an error when passing an invalid resource', async () => { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 70824995ac..58d2bc9c6d 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -78,21 +78,21 @@ export async function handleAutocompleteRequest({ return { results: result.map(title => ({ title })) }; } case 'branches': { - if (!parameters.workspace || !parameters.repository) + if (!context.workspace || !context.repository) throw new InputError( - 'Missing workspace and/or repository query parameter', + 'Missing workspace and/or repository context parameter', ); const result: string[] = []; for await (const page of client - .listBranchesByRepository(parameters.repository, parameters.workspace) + .listBranchesByRepository(context.repository, context.workspace) .iteratePages()) { const names = [...page.values!].map(p => p.name!); result.push(...names); } - return result; + return { results: result.map(title => ({ title })) }; } default: throw new InputError(`Invalid resource: ${resource}`); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx index 6cbc561bc9..638a05b4ce 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx @@ -26,7 +26,9 @@ import userEvent from '@testing-library/user-event'; describe('BitbucketRepoBranchPicker', () => { const scaffolderApiMock: Partial = { - autocomplete: jest.fn().mockResolvedValue(['branch1']), + autocomplete: jest + .fn() + .mockResolvedValue({ results: [{ title: 'branch1' }] }), }; it('renders an input field', () => { diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index 76de25dba0..ed54cddc5e 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -57,16 +57,17 @@ export const BitbucketRepoBranchPicker = ({ host === 'bitbucket.org' && accessToken && workspace && - repository + repository && + scaffolderApi.autocomplete ) { - const result = await scaffolderApi.autocomplete( - accessToken, - 'bitbucketCloud', - 'branches', - { workspace, repository }, - ); + const { results } = await scaffolderApi.autocomplete({ + token: accessToken, + resource: 'branches', + context: { workspace, repository }, + provider: 'bitbucket-cloud', + }); - setAvailableBranches(result); + setAvailableBranches(results.map(r => r.title)); } else { setAvailableBranches([]); } From caf7fc23c3f172a9744b9bbf72ff9a92f8094e10 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 28 Jun 2024 13:16:50 +0200 Subject: [PATCH 15/24] docs: extend documentation Signed-off-by: Benjamin Janssens --- .../software-templates/ui-options-examples.md | 27 ++++++++++++++++++ .../software-templates/writing-templates.md | 28 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/docs/features/software-templates/ui-options-examples.md b/docs/features/software-templates/ui-options-examples.md index 7f0e5b225f..1a024569a6 100644 --- a/docs/features/software-templates/ui-options-examples.md +++ b/docs/features/software-templates/ui-options-examples.md @@ -441,3 +441,30 @@ repoUrl: `secretsKey` is the key used within the template secrets context to store the credential and `additionalScopes` is any additional permission scopes to request. The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. + +## RepoBranchPicker + +The input props that can be specified under `ui:options` for the `RepoBranchPicker` field extension. + +### `requestUserCredentials` + +If defined will request user credentials to auth against the given SCM platform. + +```yaml +repoUrl: + title: Repository Branch + type: string + ui:field: RepoBranchPicker + ui:options: + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN + additionalScopes: + github: + - workflow:write +``` + +`secretsKey` is the key used within the template secrets context to store the credential and `additionalScopes` is any additional permission scopes to request. + +The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. + +If you're also using the `RepoUrlPicker` field extension, you should simply duplicate this part from there. diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 15591eeb67..12bf65765a 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -477,6 +477,34 @@ template can be published to multiple providers. Note, that you will need to configure an [authentication provider](../../auth/index.md#configuring-authentication-providers), alongside the [`ScmAuthApi`](../../auth/index.md#scaffolder-configuration-software-templates) for your source code management (SCM) service to make this feature work. +### The Repository Branch Picker + +Similar to the repository picker, there is a picker for branches to support autocompletion. A full example could look like this: + +```yaml +- title: Choose a branch + required: + - repoBranch + properties: + repoBranch: + title: Repository Branch + type: string + ui:field: RepoBranchPicker + ui:options: + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN +``` + +Passing the `requestUserCredentials` object is required for autocompletion to work. +If you're also using the repository picker, you should simply duplicate this part from there. +For more information regarding the `requestUserCredentials` object, please refer to the [Using the Users `oauth` token](#using-the-users-oauth-token) section under [The Repository Picker](#the-repository-picker). + +For a list of all possible `ui:options` input props for `RepoBranchPicker`, please visit [here](./ui-options-examples.md#repobranchpicker). + +The `RepoBranchPicker` is a custom field that we provide part of the +`plugin-scaffolder`. You can provide your own custom fields by +[writing your own Custom Field Extensions](./writing-custom-field-extensions.md) + ### Accessing the signed-in users details Sometimes when authoring templates, you'll want to access the user that is running the template, and get details from the profile or the users `Entity` in the Catalog. From 3fca64320d18d8093557ccc666a3c3769b8d9535 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 28 Jun 2024 13:24:57 +0200 Subject: [PATCH 16/24] chore: add changesets Signed-off-by: Benjamin Janssens --- .changeset/fast-bulldogs-relax.md | 5 +++++ .changeset/tough-goats-hang.md | 5 +++++ .changeset/witty-timers-marry.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/fast-bulldogs-relax.md create mode 100644 .changeset/tough-goats-hang.md create mode 100644 .changeset/witty-timers-marry.md diff --git a/.changeset/fast-bulldogs-relax.md b/.changeset/fast-bulldogs-relax.md new file mode 100644 index 0000000000..65895345e9 --- /dev/null +++ b/.changeset/fast-bulldogs-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +--- + +Added autocompletion support for resource `branches` diff --git a/.changeset/tough-goats-hang.md b/.changeset/tough-goats-hang.md new file mode 100644 index 0000000000..6090c582d0 --- /dev/null +++ b/.changeset/tough-goats-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added field extension `RepoBranchPicker` diff --git a/.changeset/witty-timers-marry.md b/.changeset/witty-timers-marry.md new file mode 100644 index 0000000000..4193acb6fc --- /dev/null +++ b/.changeset/witty-timers-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bitbucket-cloud-common': patch +--- + +Added method `listBranchesByRepository` to `BitbucketCloudClient` From 1ae05800608f8e39c2b1e4eaec8efeeff724ff5c Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 28 Jun 2024 13:26:31 +0200 Subject: [PATCH 17/24] chore: update API reports Signed-off-by: Benjamin Janssens --- plugins/bitbucket-cloud-common/api-report.md | 9 ++++++++ plugins/scaffolder/api-report.md | 22 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index 34c245facc..833a367c03 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -12,6 +12,12 @@ export class BitbucketCloudClient { config: BitbucketCloudIntegrationConfig, ): BitbucketCloudClient; // (undocumented) + listBranchesByRepository( + repository: string, + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination; + // (undocumented) listProjectsByWorkspace( workspace: string, options?: FilterAndSortOptions & PartialResponseOptions, @@ -209,6 +215,9 @@ export namespace Models { size?: number; values?: Array | Set; } + export interface PaginatedBranches extends Paginated { + values?: Set; + } export interface PaginatedProjects extends Paginated { values?: Set; } diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index f479b13ec4..6641cda26b 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -384,6 +384,28 @@ export const OwnerPickerFieldSchema: FieldSchema< // @public export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType; +// @public +export const RepoBranchPickerFieldExtension: FieldExtensionComponent_2< + string, + { + requestUserCredentials?: + | { + secretsKey: string; + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + gitea?: string[] | undefined; + } + | undefined; + } + | undefined; + } +>; + // @public export const repoPickerValidation: ( value: string, From 362ae34cb4b9a87a8a3e41f5a7c94bcbee7eb346 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 4 Jul 2024 13:53:18 +0200 Subject: [PATCH 18/24] refactor: extract BaseRepoBranchPickerProps Signed-off-by: Benjamin Janssens --- .../RepoBranchPicker/BitbucketRepoBranchPicker.tsx | 10 +++------- .../RepoBranchPicker/DefaultRepoBranchPicker.tsx | 9 ++------- .../src/components/fields/RepoBranchPicker/types.ts | 7 +++++++ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index ed54cddc5e..f6e6634ed8 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -21,7 +21,7 @@ import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; import { useApi } from '@backstage/core-plugin-api'; -import { RepoBranchPickerState } from './types'; +import { BaseRepoBranchPickerProps } from './types'; import FormHelperText from '@material-ui/core/FormHelperText'; /** @@ -37,13 +37,9 @@ export const BitbucketRepoBranchPicker = ({ rawErrors, accessToken, required, -}: { - onChange: (state: RepoBranchPickerState) => void; - state: RepoBranchPickerState; - rawErrors: string[]; +}: BaseRepoBranchPickerProps<{ accessToken?: string; - required?: boolean; -}) => { +}>) => { const { host, workspace, repository, branch } = state; const [availableBranches, setAvailableBranches] = useState([]); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx index 52a4a15e23..9350e232f8 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx @@ -20,7 +20,7 @@ import FormHelperText from '@material-ui/core/FormHelperText'; import Input from '@material-ui/core/Input'; import InputLabel from '@material-ui/core/InputLabel'; -import { RepoBranchPickerState } from './types'; +import { BaseRepoBranchPickerProps } from './types'; /** * The underlying component that is rendered in the form for the `DefaultRepoBranchPicker` @@ -34,12 +34,7 @@ export const DefaultRepoBranchPicker = ({ state, rawErrors, required, -}: { - onChange: (state: RepoBranchPickerState) => void; - state: RepoBranchPickerState; - rawErrors: string[]; - required?: boolean; -}) => { +}: BaseRepoBranchPickerProps) => { const { branch } = state; return ( diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts index 1d54ea7e8b..bd218d9e28 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts @@ -20,3 +20,10 @@ export interface RepoBranchPickerState { repository?: string; branch?: string; } + +export type BaseRepoBranchPickerProps = T & { + onChange: (state: RepoBranchPickerState) => void; + state: RepoBranchPickerState; + rawErrors: string[]; + required?: boolean; +}; From 3cea09fcf2c9cb8a990ff7ddd9be42174e8958c6 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 4 Jul 2024 14:09:27 +0200 Subject: [PATCH 19/24] chore: remove unnecessary mapping from handleAutocompleteRequest Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 58d2bc9c6d..75cc170a30 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -34,29 +34,29 @@ export async function handleAutocompleteRequest({ switch (resource) { case 'workspaces': { - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client.listWorkspaces().iteratePages()) { - const slugs = [...page.values!].map(p => p.slug!); - result.push(...slugs); + const slugs = [...page.values!].map(p => ({ title: p.slug! })); + results.push(...slugs); } - return { results: result.map(title => ({ title })) }; + return { results }; } case 'projects': { if (!context.workspace) throw new InputError('Missing workspace context parameter'); - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client .listProjectsByWorkspace(context.workspace) .iteratePages()) { - const keys = [...page.values!].map(p => p.key!); - result.push(...keys); + const keys = [...page.values!].map(p => ({ title: p.key! })); + results.push(...keys); } - return { results: result.map(title => ({ title })) }; + return { results }; } case 'repositories': { if (!context.workspace || !context.project) @@ -64,18 +64,18 @@ export async function handleAutocompleteRequest({ 'Missing workspace and/or project context parameter', ); - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client .listRepositoriesByWorkspace(context.workspace, { q: `project.key="${context.project}"`, }) .iteratePages()) { - const slugs = [...page.values!].map(p => p.slug!); - result.push(...slugs); + const slugs = [...page.values!].map(p => ({ title: p.slug! })); + results.push(...slugs); } - return { results: result.map(title => ({ title })) }; + return { results }; } case 'branches': { if (!context.workspace || !context.repository) @@ -83,16 +83,16 @@ export async function handleAutocompleteRequest({ 'Missing workspace and/or repository context parameter', ); - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client .listBranchesByRepository(context.repository, context.workspace) .iteratePages()) { - const names = [...page.values!].map(p => p.name!); - result.push(...names); + const names = [...page.values!].map(p => ({ title: p.name! })); + results.push(...names); } - return { results: result.map(title => ({ title })) }; + return { results }; } default: throw new InputError(`Invalid resource: ${resource}`); From 618aa1454890c9c2c499f3a2c178993978308a95 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 4 Jul 2024 14:14:20 +0200 Subject: [PATCH 20/24] refactor: use useCallback for updateAvailableBranches Co-authored-by: Camila Belo Signed-off-by: Benjamin Janssens --- .../BitbucketRepoBranchPicker.tsx | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index f6e6634ed8..76b8d071ea 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -46,34 +46,34 @@ export const BitbucketRepoBranchPicker = ({ const scaffolderApi = useApi(scaffolderApiRef); - useDebounce( - () => { - const updateAvailableBranches = async () => { - if ( - host === 'bitbucket.org' && - accessToken && - workspace && - repository && - scaffolderApi.autocomplete - ) { - const { results } = await scaffolderApi.autocomplete({ - token: accessToken, - resource: 'branches', - context: { workspace, repository }, - provider: 'bitbucket-cloud', - }); + const updateAvailableBranches = useCallback(() => { + if ( + !scaffolderApi.autocomplete || + !workspace || + !repository || + !accessToken || + host !== 'bitbucket.org' + ) { + setAvailableBranches([]); + return; + } - setAvailableBranches(results.map(r => r.title)); - } else { - setAvailableBranches([]); - } - }; + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'branches', + context: { workspace, repository }, + provider: 'bitbucket-cloud', + }) + .then(({ results }) => { + setAvailableBranches(results.map(r => r.title)); + }) + .catch(() => { + setAvailableBranches([]); + }); + }, [host, workspace, repository, accessToken, scaffolderApi]); - updateAvailableBranches().catch(() => setAvailableBranches([])); - }, - 500, - [host, workspace, repository, accessToken], - ); + useDebounce(updateAvailableBranches, 500, [updateAvailableBranches]); return ( Date: Thu, 4 Jul 2024 16:08:13 +0200 Subject: [PATCH 21/24] test: simplify checking for the secret in the document; remove SecretsComponent from test that didn't use it Signed-off-by: Benjamin Janssens --- .../RepoBranchPicker.test.tsx | 49 +++++++------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx index ece73670f6..f9c23be407 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx @@ -133,13 +133,15 @@ describe('RepoBranchPicker', () => { describe('requestUserCredentials', () => { it('should call the scmAuthApi with the correct params', async () => { + const secretsKey = 'testKey'; + const SecretsComponent = () => { const { secrets } = useTemplateSecrets(); - return ( -
{JSON.stringify({ secrets })}
- ); + const secret = secrets[secretsKey]; + return secret ?
{secret}
: null; }; - const { getByTestId } = await renderInTestApp( + + const { getByText } = await renderInTestApp( { 'ui:field': 'RepoBranchPicker', 'ui:options': { requestUserCredentials: { - secretsKey: 'testKey', + secretsKey, additionalScopes: { github: ['workflow'] }, }, }, @@ -190,22 +192,10 @@ describe('RepoBranchPicker', () => { }, }); - const currentSecrets = JSON.parse( - getByTestId('current-secrets').textContent!, - ); - - expect(currentSecrets).toEqual({ - secrets: { testKey: 'abc123' }, - }); + expect(getByText('abc123')).toBeInTheDocument(); }); it('should call the scmAuthApi with the correct params if workspace is nested', async () => { - const SecretsComponent = () => { - const { secrets } = useTemplateSecrets(); - return ( -
{JSON.stringify({ secrets })}
- ); - }; await renderInTestApp( { }, }} /> - , ); @@ -256,13 +245,15 @@ describe('RepoBranchPicker', () => { }); it('should not call the scmAuthApi if secret is available in the state', async () => { + const secretsKey = 'testKey'; + const SecretsComponent = () => { const { secrets } = useTemplateSecrets(); - return ( -
{JSON.stringify({ secrets })}
- ); + const secret = secrets[secretsKey]; + return secret ?
{secret}
: null; }; - const { getByTestId } = await renderInTestApp( + + const { getByText } = await renderInTestApp( { [scaffolderApiRef, {}], ]} > - + { 'ui:field': 'RepoBranchPicker', 'ui:options': { requestUserCredentials: { - secretsKey: 'testKey', + secretsKey, additionalScopes: { github: ['workflow'] }, }, }, @@ -306,13 +297,7 @@ describe('RepoBranchPicker', () => { // as we already have a secret in the state, getCredentials should not be called again. expect(mockScmAuthApi.getCredentials).toHaveBeenCalledTimes(0); - const currentSecrets = JSON.parse( - getByTestId('current-secrets').textContent!, - ); - - expect(currentSecrets).toEqual({ - secrets: { testKey: 'abc123' }, - }); + expect(getByText('abc123')).toBeInTheDocument(); }); }); }); From 3956205ad85fb808386edfac1dab7ff273216225 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Thu, 4 Jul 2024 16:31:01 +0200 Subject: [PATCH 22/24] fix: import useCallback in BitbucketRepoBranchPicker Signed-off-by: Benjamin Janssens --- .../fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index 76b8d071ea..26853d6d05 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -16,7 +16,7 @@ import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import FormControl from '@material-ui/core/FormControl'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import useDebounce from 'react-use/esm/useDebounce'; From be632623e5e3978660099002bc0f12dac4bf8ec3 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 09:50:42 +0200 Subject: [PATCH 23/24] chore: improve changeset for @backstage/plugin-scaffolder Signed-off-by: Benjamin Janssens --- .changeset/tough-goats-hang.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tough-goats-hang.md b/.changeset/tough-goats-hang.md index 6090c582d0..2608c03b2e 100644 --- a/.changeset/tough-goats-hang.md +++ b/.changeset/tough-goats-hang.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': minor --- -Added field extension `RepoBranchPicker` +Added field extension `RepoBranchPicker` that supports autocompletion for Bitbucket From 5436d971d92dd9d60645e4bdcc4869de5405bb5f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 15 Jul 2024 09:51:52 +0200 Subject: [PATCH 24/24] chore: add comment to schema to link duplicated comments together Signed-off-by: Benjamin Janssens --- .../scaffolder/src/components/fields/RepoBranchPicker/schema.ts | 1 + plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts index 6ee67bd1c7..fae5e6435e 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts @@ -77,6 +77,7 @@ export type RepoBranchPickerUiOptions = export type RepoBranchPickerProps = typeof RepoBranchPickerFieldSchema.type; +// This has been duplicated from /plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts // NOTE: There is a bug with this failing validation in the custom field explorer due // to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if // requestUserCredentials is not defined diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts index 2a4691380f..2ec72bc5a6 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts @@ -97,6 +97,7 @@ export type RepoUrlPickerUiOptions = export type RepoUrlPickerProps = typeof RepoUrlPickerFieldSchema.type; +// This has been duplicated to /plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts // NOTE: There is a bug with this failing validation in the custom field explorer due // to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if // requestUserCredentials is not defined