refactor: add generic RepoBranchPicker
Signed-off-by: Benjamin Janssens <benji.janssens@gmail.com>
This commit is contained in:
+26
-83
@@ -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<string>();
|
||||
const [workspace, setWorkspace] = useState<string | null>(null);
|
||||
const [repository, setRepository] = useState<string | null>(null);
|
||||
const [availableBranches, setAvailableBranches] = useState<string[]>([]);
|
||||
|
||||
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 (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required={required}
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
required
|
||||
error={rawErrors?.length > 0 && !state.branch}
|
||||
>
|
||||
<Autocomplete
|
||||
onChange={(_, newValue) => {
|
||||
onChange(newValue || '');
|
||||
onChange({ branch: newValue || undefined });
|
||||
}}
|
||||
options={availableBranches}
|
||||
options={state.availableBranches || []}
|
||||
renderInput={params => (
|
||||
<TextField {...params} label="Branch" required={required} />
|
||||
<TextField {...params} label="Branch" required />
|
||||
)}
|
||||
freeSolo
|
||||
autoSelect
|
||||
|
||||
+125
@@ -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<RepoBranchPickerState>({});
|
||||
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 && (
|
||||
<Box my={1}>
|
||||
<Typography variant="h5">{schema.title}</Typography>
|
||||
<Divider />
|
||||
</Box>
|
||||
)}
|
||||
{schema.description && (
|
||||
<Typography variant="body1">{schema.description}</Typography>
|
||||
)}
|
||||
{hostType === 'bitbucket' && (
|
||||
<BitbucketRepoBranchPicker
|
||||
onChange={updateLocalState}
|
||||
state={state}
|
||||
rawErrors={rawErrors}
|
||||
accessToken={
|
||||
uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey &&
|
||||
secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -15,6 +15,6 @@
|
||||
*/
|
||||
|
||||
export {
|
||||
BitbucketRepoBranchPickerFieldSchema,
|
||||
type BitbucketRepoBranchPickerUiOptions,
|
||||
RepoBranchPickerFieldSchema,
|
||||
type RepoBranchPickerUiOptions,
|
||||
} from './schema';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -30,7 +30,7 @@ export {
|
||||
MyGroupsPickerFieldExtension,
|
||||
RepoUrlPickerFieldExtension,
|
||||
MultiEntityPickerFieldExtension,
|
||||
BitbucketRepoBranchPickerFieldExtension,
|
||||
RepoBranchPickerFieldExtension,
|
||||
ScaffolderPage,
|
||||
scaffolderPlugin,
|
||||
} from './plugin';
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user