feat: add BitbucketRepoBranchPicker

Signed-off-by: Benjamin Janssens <benji.janssens@gmail.com>
This commit is contained in:
Benjamin Janssens
2024-06-17 16:32:15 +02:00
parent 7ee5682ad4
commit f6fea34173
6 changed files with 270 additions and 0 deletions
@@ -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<string>();
const [workspace, setWorkspace] = useState<string | null>(null);
const [repository, setRepository] = useState<string | null>(null);
const [availableBranches, setAvailableBranches] = useState<string[]>([]);
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 (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
>
<Autocomplete
onChange={(_, newValue) => {
onChange(newValue || '');
}}
options={availableBranches}
renderInput={params => (
<TextField {...params} label="Branch" required={required} />
)}
freeSolo
autoSelect
/>
</FormControl>
);
};
@@ -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';
@@ -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;
@@ -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,
},
];
+1
View File
@@ -30,6 +30,7 @@ export {
MyGroupsPickerFieldExtension,
RepoUrlPickerFieldExtension,
MultiEntityPickerFieldExtension,
BitbucketRepoBranchPickerFieldExtension,
ScaffolderPage,
scaffolderPlugin,
} from './plugin';
+15
View File
@@ -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,
}),
);