Merge branch 'master' into feat/myGroupsPickerPresentationApi

This commit is contained in:
Julien
2024-08-12 09:03:26 +02:00
666 changed files with 20474 additions and 4077 deletions
+25
View File
@@ -1,5 +1,30 @@
# @backstage/plugin-scaffolder
## 1.24.0-next.2
### Minor Changes
- 3fca643: Added field extension `RepoBranchPicker` that supports autocompletion for Bitbucket
### Patch Changes
- Updated dependencies
- @backstage/plugin-scaffolder-react@1.11.0-next.2
- @backstage/frontend-plugin-api@0.7.0-next.2
- @backstage/core-compat-api@0.2.8-next.2
- @backstage/plugin-catalog-react@1.12.3-next.2
- @backstage/plugin-catalog-common@1.0.26-next.1
- @backstage/plugin-permission-react@0.4.25-next.1
- @backstage/plugin-scaffolder-common@1.5.5-next.1
- @backstage/integration@1.14.0-next.0
- @backstage/catalog-client@1.6.5
- @backstage/catalog-model@1.5.0
- @backstage/core-components@0.14.10-next.0
- @backstage/core-plugin-api@1.9.3
- @backstage/errors@1.2.4
- @backstage/integration-react@1.1.30-next.0
- @backstage/types@1.1.1
## 1.23.1-next.1
### Patch Changes
+8 -10
View File
@@ -30,16 +30,14 @@ const _default: BackstagePlugin<
edit: SubRouteRef<undefined>;
},
{
registerComponent: ExternalRouteRef<undefined, true>;
viewTechDoc: ExternalRouteRef<
{
name: string;
kind: string;
namespace: string;
},
true
>;
}
registerComponent: ExternalRouteRef<undefined>;
viewTechDoc: ExternalRouteRef<{
name: string;
kind: string;
namespace: string;
}>;
},
{}
>;
export default _default;
+22
View File
@@ -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,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-scaffolder",
"version": "1.23.1-next.1",
"version": "1.24.0-next.2",
"description": "The Backstage plugin that helps you create new things",
"backstage": {
"role": "frontend-plugin",
@@ -173,7 +173,9 @@ export const OngoingTask = (props: {
return (
<Page themeId="website">
<Header
pageTitleOverride={`Run of ${templateName}`}
pageTitleOverride={
templateName ? `Run of ${templateName}` : `Scaffolder Run`
}
title={
<div>
Run of <code>{templateName}</code>
@@ -0,0 +1,107 @@
/*
* 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<ScaffolderApi> = {
autocomplete: jest
.fn()
.mockResolvedValue({ results: [{ title: 'branch1' }] }),
};
it('renders an input field', () => {
const { getByRole } = render(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoBranchPicker
onChange={jest.fn()}
state={{ branch: 'main' }}
rawErrors={[]}
/>
</TestApiProvider>,
);
expect(getByRole('textbox')).toBeInTheDocument();
expect(getByRole('textbox')).toHaveValue('main');
});
it('calls onChange when the input field changes', () => {
const onChange = jest.fn();
const { getByRole } = render(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoBranchPicker
onChange={onChange}
state={{ branch: 'main' }}
rawErrors={[]}
/>
</TestApiProvider>,
);
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(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<BitbucketRepoBranchPicker
onChange={onChange}
state={{
branch: 'main',
host: 'bitbucket.org',
workspace: 'foo',
repository: 'bar',
}}
rawErrors={[]}
accessToken="token"
/>
</TestApiProvider>,
);
// 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',
});
});
});
@@ -0,0 +1,99 @@
/*
* 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 } from '@backstage/plugin-scaffolder-react';
import FormControl from '@material-ui/core/FormControl';
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';
import { useApi } from '@backstage/core-plugin-api';
import { BaseRepoBranchPickerProps } 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,
rawErrors,
accessToken,
required,
}: BaseRepoBranchPickerProps<{
accessToken?: string;
}>) => {
const { host, workspace, repository, branch } = state;
const [availableBranches, setAvailableBranches] = useState<string[]>([]);
const scaffolderApi = useApi(scaffolderApiRef);
const updateAvailableBranches = useCallback(() => {
if (
!scaffolderApi.autocomplete ||
!workspace ||
!repository ||
!accessToken ||
host !== 'bitbucket.org'
) {
setAvailableBranches([]);
return;
}
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]);
useDebounce(updateAvailableBranches, 500, [updateAvailableBranches]);
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !branch}
>
<Autocomplete
value={branch}
onChange={(_, newValue) => {
onChange({ branch: newValue || '' });
}}
options={availableBranches}
renderInput={params => (
<TextField {...params} label="Branch" required={required} />
)}
freeSolo
autoSelect
/>
<FormHelperText>The branch of the repository</FormHelperText>
</FormControl>
);
};
@@ -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(
<DefaultRepoBranchPicker
onChange={jest.fn()}
state={{ branch: 'main' }}
rawErrors={[]}
/>,
);
expect(getByRole('textbox')).toBeInTheDocument();
expect(getByRole('textbox')).toHaveValue('main');
});
it('calls onChange when the input field changes', () => {
const onChange = jest.fn();
const { getByRole } = render(
<DefaultRepoBranchPicker
onChange={onChange}
state={{ branch: 'main' }}
rawErrors={[]}
/>,
);
const input = getByRole('textbox');
fireEvent.change(input, {
target: { value: 'develop' },
});
expect(onChange).toHaveBeenCalledWith({ branch: 'develop' });
});
});
@@ -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 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 { BaseRepoBranchPickerProps } from './types';
/**
* The underlying component that is rendered in the form for the `DefaultRepoBranchPicker`
* field extension.
*
* @public
*
*/
export const DefaultRepoBranchPicker = ({
onChange,
state,
rawErrors,
required,
}: BaseRepoBranchPickerProps) => {
const { branch } = state;
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !branch}
>
<InputLabel htmlFor="branchInput">Branch</InputLabel>
<Input
id="branchInput"
onChange={e => onChange({ branch: e.target.value })}
value={branch}
/>
<FormHelperText>The branch of the repository</FormHelperText>
</FormControl>
);
};
@@ -0,0 +1,303 @@
/*
* 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<ScmIntegrationsApi> = {
byHost: () => ({ type: 'bitbucket' }),
};
let mockScmAuthApi: Partial<ScmAuthApi>;
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(
<TestApiProvider
apis={[
[scmIntegrationsApiRef, mockIntegrationsApi],
[scmAuthApiRef, {}],
[scaffolderApiRef, {}],
]}
>
<SecretsContextProvider>
<Form
validator={validator}
schema={{ type: 'string' }}
uiSchema={{ 'ui:field': 'RepoBranchPicker' }}
fields={{
RepoBranchPicker:
RepoBranchPicker as ScaffolderRJSFField<string>,
}}
onSubmit={onSubmit}
formContext={{
formData: {},
}}
/>
</SecretsContextProvider>
</TestApiProvider>,
);
const input = getByRole('textbox');
const submitButton = getByRole('button');
fireEvent.change(input, { target: { value: 'branch1' } });
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(
<TestApiProvider
apis={[
[scmIntegrationsApiRef, mockIntegrationsApi],
[scmAuthApiRef, {}],
[scaffolderApiRef, {}],
]}
>
<SecretsContextProvider>
<Form
validator={validator}
schema={{
type: 'string',
title: 'test title',
description: 'test description',
}}
uiSchema={{
'ui:field': 'RepoBranchPicker',
}}
fields={{
RepoBranchPicker:
RepoBranchPicker as ScaffolderRJSFField<string>,
}}
formContext={{
formData: {
repoUrl: 'bitbucket.org',
},
}}
/>
</SecretsContextProvider>
</TestApiProvider>,
);
expect(getByText('test title')).toBeInTheDocument();
expect(getByText('test description')).toBeInTheDocument();
});
});
describe('requestUserCredentials', () => {
it('should call the scmAuthApi with the correct params', async () => {
const secretsKey = 'testKey';
const SecretsComponent = () => {
const { secrets } = useTemplateSecrets();
const secret = secrets[secretsKey];
return secret ? <div>{secret}</div> : null;
};
const { getByText } = await renderInTestApp(
<TestApiProvider
apis={[
[scmIntegrationsApiRef, mockIntegrationsApi],
[scmAuthApiRef, mockScmAuthApi],
[scaffolderApiRef, {}],
]}
>
<SecretsContextProvider>
<Form
validator={validator}
schema={{ type: 'string' }}
uiSchema={{
'ui:field': 'RepoBranchPicker',
'ui:options': {
requestUserCredentials: {
secretsKey,
additionalScopes: { github: ['workflow'] },
},
},
}}
fields={{
RepoBranchPicker:
RepoBranchPicker as ScaffolderRJSFField<string>,
}}
formContext={{
formData: {
repoUrl: 'github.com',
},
}}
/>
<SecretsComponent />
</SecretsContextProvider>
</TestApiProvider>,
);
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'],
},
},
});
expect(getByText('abc123')).toBeInTheDocument();
});
it('should call the scmAuthApi with the correct params if workspace is nested', async () => {
await renderInTestApp(
<TestApiProvider
apis={[
[scmIntegrationsApiRef, mockIntegrationsApi],
[scmAuthApiRef, mockScmAuthApi],
[scaffolderApiRef, {}],
]}
>
<SecretsContextProvider>
<Form
validator={validator}
schema={{ type: 'string' }}
uiSchema={{
'ui:field': 'RepoBranchPicker',
'ui:options': {
allowedHosts: ['gitlab.example.com'],
requestUserCredentials: {
secretsKey: 'testKey',
},
},
}}
fields={{
RepoBranchPicker:
RepoBranchPicker as ScaffolderRJSFField<string>,
}}
formContext={{
formData: {
repoUrl: 'gitlab.example.com',
},
}}
/>
</SecretsContextProvider>
</TestApiProvider>,
);
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 secretsKey = 'testKey';
const SecretsComponent = () => {
const { secrets } = useTemplateSecrets();
const secret = secrets[secretsKey];
return secret ? <div>{secret}</div> : null;
};
const { getByText } = await renderInTestApp(
<TestApiProvider
apis={[
[scmIntegrationsApiRef, mockIntegrationsApi],
[scmAuthApiRef, mockScmAuthApi],
[scaffolderApiRef, {}],
]}
>
<SecretsContextProvider initialSecrets={{ [secretsKey]: 'abc123' }}>
<Form
validator={validator}
schema={{ type: 'string' }}
uiSchema={{
'ui:field': 'RepoBranchPicker',
'ui:options': {
requestUserCredentials: {
secretsKey,
additionalScopes: { github: ['workflow'] },
},
},
}}
fields={{
RepoBranchPicker:
RepoBranchPicker as ScaffolderRJSFField<string>,
}}
formContext={{
formData: {
repoUrl: 'github.com',
},
}}
/>
<SecretsComponent />
</SecretsContextProvider>
</TestApiProvider>,
);
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);
expect(getByText('abc123')).toBeInTheDocument();
});
});
});
@@ -0,0 +1,163 @@
/*
* 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 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 { 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`
* field extension.
*
* @public
*/
export const RepoBranchPicker = (props: RepoBranchPickerProps) => {
const {
uiSchema,
onChange,
rawErrors,
formData,
schema,
formContext,
required,
} = props;
const {
formData: { repoUrl },
} = formContext;
const [state, setState] = useState<RepoBranchPickerState>({
branch: formData || '',
});
const { host, branch } = state;
const integrationApi = useApi(scmIntegrationsApiRef);
const scmAuthApi = useApi(scmAuthApiRef);
const { secrets, setSecrets } = useTemplateSecrets();
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],
);
useEffect(() => {
if (repoUrl) {
const url = new URL(`https://${repoUrl}`);
setState(prevState => ({
...prevState,
host: url.host,
workspace: url.searchParams.get('workspace') || '',
repository: url.searchParams.get('repo') || '',
}));
}
}, [repoUrl]);
useEffect(() => {
onChange(branch);
}, [branch, onChange]);
const updateLocalState = useCallback(
(newState: RepoBranchPickerState) => {
setState(prevState => ({ ...prevState, ...newState }));
},
[setState],
);
const hostType = (host && integrationApi.byHost(host)?.type) ?? null;
const renderRepoBranchPicker = () => {
switch (hostType) {
case 'bitbucket':
return (
<BitbucketRepoBranchPicker
onChange={updateLocalState}
state={state}
rawErrors={rawErrors}
accessToken={
uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey &&
secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey]
}
required={required}
/>
);
default:
return (
<DefaultRepoBranchPicker
onChange={updateLocalState}
state={state}
rawErrors={rawErrors}
required={required}
/>
);
}
};
return (
<>
{schema.title && (
<Box my={1}>
<Typography variant="h5">{schema.title}</Typography>
<Divider />
</Box>
)}
{schema.description && (
<Typography variant="body1">{schema.description}</Typography>
)}
{renderRepoBranchPicker()}
</>
);
};
@@ -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 {
RepoBranchPickerFieldSchema,
type RepoBranchPickerUiOptions,
} from './schema';
@@ -0,0 +1,84 @@
/*
* 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 RepoBranchPickerFieldSchema = 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
* `RepoBranchPicker` field extension.
*
* @public
*/
export type RepoBranchPickerUiOptions =
typeof RepoBranchPickerFieldSchema.uiOptionsType;
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
export const RepoBranchPickerSchema = RepoBranchPickerFieldSchema.schema;
@@ -0,0 +1,29 @@
/*
* 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;
}
export type BaseRepoBranchPickerProps<T extends {} = {}> = T & {
onChange: (state: RepoBranchPickerState) => void;
state: RepoBranchPickerState;
rawErrors: string[];
required?: boolean;
};
@@ -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
@@ -50,6 +50,8 @@ import {
MultiEntityPickerSchema,
validateMultiEntityPickerValidation,
} from '../components/fields/MultiEntityPicker/MultiEntityPicker';
import { RepoBranchPicker } from '../components/fields/RepoBranchPicker/RepoBranchPicker';
import { RepoBranchPickerSchema } from '../components/fields/RepoBranchPicker/schema';
export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [
{
@@ -99,4 +101,9 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [
schema: MultiEntityPickerSchema,
validation: validateMultiEntityPickerValidation,
},
{
component: RepoBranchPicker,
name: 'RepoBranchPicker',
schema: RepoBranchPickerSchema,
},
];
+1
View File
@@ -30,6 +30,7 @@ export {
MyGroupsPickerFieldExtension,
RepoUrlPickerFieldExtension,
MultiEntityPickerFieldExtension,
RepoBranchPickerFieldExtension,
ScaffolderPage,
scaffolderPlugin,
} from './plugin';
+15
View File
@@ -73,6 +73,8 @@ import {
MyGroupsPicker,
MyGroupsPickerSchema,
} from './components/fields/MyGroupsPicker/MyGroupsPicker';
import { RepoBranchPicker } from './components/fields/RepoBranchPicker/RepoBranchPicker';
import { RepoBranchPickerSchema } from './components/fields/RepoBranchPicker/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 RepoBranchPickerFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
component: RepoBranchPicker,
name: 'RepoBranchPicker',
schema: RepoBranchPickerSchema,
}),
);