From 741837a560f16369b3aadce4294e02726e5732e8 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 18 Jun 2024 11:42:20 +0200 Subject: [PATCH] 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, }), );