Merge pull request #28706 from benjidotsh/scaffolder/autocomplete-github

feat(scaffolder): add support for autocompletion to `GitHubRepoPicker`
This commit is contained in:
Ben Lambert
2025-02-04 15:08:03 +01:00
committed by GitHub
29 changed files with 689 additions and 260 deletions
+17
View File
@@ -0,0 +1,17 @@
---
'@backstage/plugin-scaffolder-backend-module-github': patch
---
**DEPRECATION**: The `getOctokitOptions` function signature with `repoUrl` option has been deprecated in favour of a function signature with individual `host`, `owner`, and `repo` parameters:
```diff
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider,
token,
- repoUrl,
+ host,
+ owner,
+ repo,
});
```
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': minor
'@backstage/plugin-scaffolder-backend-module-github': patch
---
Added support for autocompletion to GithubRepoPicker component
@@ -474,6 +474,16 @@ export const createPublishGithubPullRequestAction: (
>;
// @public
export function getOctokitOptions(options: {
integrations: ScmIntegrationRegistry;
credentialsProvider?: GithubCredentialsProvider;
token?: string;
host: string;
owner?: string;
repo?: string;
}): Promise<OctokitOptions>;
// @public @deprecated
export function getOctokitOptions(options: {
integrations: ScmIntegrationRegistry;
credentialsProvider?: GithubCredentialsProvider;
@@ -27,9 +27,9 @@ import {
} from '@backstage/plugin-scaffolder-node';
import {
createGithubRepoWithCollaboratorsAndTopics,
getOctokitOptions,
initRepoPushAndProtect,
} from './helpers';
import { getOctokitOptions } from '../util';
import * as inputProps from './inputProperties';
import * as outputProps from './outputProperties';
import { examples } from './github.examples';
@@ -225,20 +225,22 @@ export function createPublishGithubAction(options: {
requiredLinearHistory = false,
} = ctx.input;
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
repoUrl: repoUrl,
});
const client = new Octokit(octokitOptions);
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
}
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
host,
owner,
repo,
});
const client = new Octokit(octokitOptions);
const newRepo = await createGithubRepoWithCollaboratorsAndTopics(
client,
repo,
@@ -24,7 +24,7 @@ import {
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { Octokit } from 'octokit';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
import { examples } from './githubActionsDispatch.examples';
/**
@@ -96,7 +96,7 @@ export function createGithubActionsDispatchAction(options: {
`Dispatching workflow ${workflowId} for repo ${repoUrl} on ${branchOrTagName}`,
);
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
@@ -105,7 +105,9 @@ export function createGithubActionsDispatchAction(options: {
const client = new Octokit(
await getOctokitOptions({
integrations,
repoUrl,
host,
owner,
repo,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
}),
@@ -25,7 +25,7 @@ import {
} from '@backstage/plugin-scaffolder-node';
import { Octokit } from 'octokit';
import { examples } from './githubAutolinks.examples';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
/**
* Create an autolink reference for a repository
@@ -89,7 +89,7 @@ export function createGithubAutolinksAction(options: {
ctx.logger.info(`Creating autolink reference for repo ${repoUrl}`);
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
@@ -98,7 +98,9 @@ export function createGithubAutolinksAction(options: {
const client = new Octokit(
await getOctokitOptions({
integrations,
repoUrl,
host,
owner,
repo,
credentialsProvider: githubCredentialsProvider,
token,
}),
@@ -22,7 +22,7 @@ import {
import { ScmIntegrationRegistry } from '@backstage/integration';
import { examples } from './githubBranchProtection.examples';
import * as inputProps from './inputProperties';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
import { Octokit } from 'octokit';
import { enableBranchProtectionOnDefaultRepoBranch } from './gitHelpers';
@@ -115,19 +115,21 @@ export function createGithubBranchProtectionAction(options: {
token: providedToken,
} = ctx.input;
const octokitOptions = await getOctokitOptions({
integrations,
token: providedToken,
repoUrl: repoUrl,
});
const client = new Octokit(octokitOptions);
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError(`No owner provided for repo ${repoUrl}`);
}
const octokitOptions = await getOctokitOptions({
integrations,
token: providedToken,
host,
owner,
repo,
});
const client = new Octokit(octokitOptions);
const repository = await client.rest.repos.get({
owner: owner,
repo: repo,
@@ -20,7 +20,7 @@ import {
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
import { Octokit } from 'octokit';
import Sodium from 'libsodium-wrappers';
import { examples } from './githubDeployKey.examples';
@@ -107,18 +107,20 @@ export function createGithubDeployKeyAction(options: {
token: providedToken,
} = ctx.input;
const octokitOptions = await getOctokitOptions({
integrations,
token: providedToken,
repoUrl: repoUrl,
});
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError(`No owner provided for repo ${repoUrl}`);
}
const octokitOptions = await getOctokitOptions({
integrations,
token: providedToken,
host,
owner,
repo,
});
const client = new Octokit(octokitOptions);
await client.rest.repos.createDeployKey({
@@ -20,7 +20,7 @@ import {
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
import { Octokit } from 'octokit';
import Sodium from 'libsodium-wrappers';
import { examples } from './gitHubEnvironment.examples';
@@ -175,18 +175,20 @@ export function createGithubEnvironmentAction(options: {
// Add a 2-second delay before initiating the steps in this action.
await new Promise(resolve => setTimeout(resolve, 2000));
const octokitOptions = await getOctokitOptions({
integrations,
token: providedToken,
repoUrl: repoUrl,
});
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError(`No owner provided for repo ${repoUrl}`);
}
const octokitOptions = await getOctokitOptions({
integrations,
token: providedToken,
host,
owner,
repo,
});
const client = new Octokit(octokitOptions);
const repository = await client.rest.repos.get({
owner: owner,
@@ -25,9 +25,9 @@ import {
import { createGithubIssuesLabelAction } from './githubIssuesLabel';
import yaml from 'yaml';
import { examples } from './githubIssuesLabel.examples';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
jest.mock('./helpers', () => {
jest.mock('../util', () => {
return {
getOctokitOptions: jest.fn(),
};
@@ -23,9 +23,9 @@ import {
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { ConfigReader } from '@backstage/config';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
jest.mock('./helpers', () => {
jest.mock('../util', () => {
return {
getOctokitOptions: jest.fn(),
};
@@ -24,7 +24,7 @@ import {
} from '@backstage/plugin-scaffolder-node';
import { assertError, InputError } from '@backstage/errors';
import { Octokit } from 'octokit';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
import { examples } from './githubIssuesLabel.examples';
/**
@@ -80,7 +80,7 @@ export function createGithubIssuesLabelAction(options: {
async handler(ctx) {
const { repoUrl, number, labels, token: providedToken } = ctx.input;
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
ctx.logger.info(`Adding labels to ${number} issue on repo ${repo}`);
if (!owner) {
@@ -91,7 +91,9 @@ export function createGithubIssuesLabelAction(options: {
await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
repoUrl: repoUrl,
host,
owner,
repo,
token: providedToken,
}),
);
@@ -25,7 +25,7 @@ import {
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { examples } from './githubPagesEnable.examples';
import { getOctokitOptions } from '@backstage/plugin-scaffolder-backend-module-github';
import { getOctokitOptions } from '../util';
/**
* Creates a new action that enables GitHub Pages for a repository.
@@ -92,20 +92,22 @@ export function createGithubPagesEnableAction(options: {
token: providedToken,
} = ctx.input;
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
repoUrl: repoUrl,
});
const client = new Octokit(octokitOptions);
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
}
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
host,
owner,
repo,
});
const client = new Octokit(octokitOptions);
ctx.logger.info(
`Attempting to enable GitHub Pages for ${owner}/${repo} with "${buildType}" build type, on source branch "${sourceBranch}" and source path "${sourcePath}"`,
);
@@ -28,7 +28,7 @@ import {
import { Octokit } from 'octokit';
import { CustomErrorBase, InputError } from '@backstage/errors';
import { createPullRequest } from 'octokit-plugin-create-pull-request';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
import { examples } from './githubPullRequest.examples';
import {
LoggerService,
@@ -49,14 +49,12 @@ export const defaultClientFactory: CreateGithubPullRequestActionOptions['clientF
host = 'github.com',
token: providedToken,
}) => {
const [encodedHost, encodedOwner, encodedRepo] = [host, owner, repo].map(
encodeURIComponent,
);
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
repoUrl: `${encodedHost}?owner=${encodedOwner}&repo=${encodedRepo}`,
host,
owner,
repo,
token: providedToken,
});
@@ -24,10 +24,8 @@ import {
createTemplateAction,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import {
createGithubRepoWithCollaboratorsAndTopics,
getOctokitOptions,
} from './helpers';
import { createGithubRepoWithCollaboratorsAndTopics } from './helpers';
import { getOctokitOptions } from '../util';
import * as inputProps from './inputProperties';
import * as outputProps from './outputProperties';
import { examples } from './githubRepoCreate.examples';
@@ -182,20 +180,22 @@ export function createGithubRepoCreateAction(options: {
token: providedToken,
} = ctx.input;
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
repoUrl: repoUrl,
});
const client = new Octokit(octokitOptions);
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
}
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
host,
owner,
repo,
});
const client = new Octokit(octokitOptions);
const newRepo = await createGithubRepoWithCollaboratorsAndTopics(
client,
repo,
@@ -25,7 +25,8 @@ import {
createTemplateAction,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { getOctokitOptions, initRepoPushAndProtect } from './helpers';
import { initRepoPushAndProtect } from './helpers';
import { getOctokitOptions } from '../util';
import * as inputProps from './inputProperties';
import * as outputProps from './outputProperties';
import { examples } from './githubRepoPush.examples';
@@ -142,7 +143,7 @@ export function createGithubRepoPushAction(options: {
requiredLinearHistory = false,
} = ctx.input;
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
@@ -152,7 +153,9 @@ export function createGithubRepoPushAction(options: {
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
repoUrl,
host,
owner,
repo,
});
const client = new Octokit(octokitOptions);
@@ -25,7 +25,7 @@ import {
import { emitterEventNames } from '@octokit/webhooks';
import { assertError, InputError } from '@backstage/errors';
import { Octokit } from 'octokit';
import { getOctokitOptions } from './helpers';
import { getOctokitOptions } from '../util';
import { examples } from './githubWebhook.examples';
/**
@@ -134,7 +134,7 @@ export function createGithubWebhookAction(options: {
} = ctx.input;
ctx.logger.info(`Creating webhook ${webhookUrl} for repo ${repoUrl}`);
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
@@ -144,7 +144,9 @@ export function createGithubWebhookAction(options: {
await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
repoUrl: repoUrl,
host,
owner,
repo,
token: providedToken,
}),
);
@@ -15,19 +15,12 @@
*/
import { Config } from '@backstage/config';
import { assertError, InputError, NotFoundError } from '@backstage/errors';
import {
DefaultGithubCredentialsProvider,
GithubCredentialsProvider,
ScmIntegrationRegistry,
} from '@backstage/integration';
import { OctokitOptions } from '@octokit/core/dist-types/types';
import { assertError, NotFoundError } from '@backstage/errors';
import { Octokit } from 'octokit';
import {
getRepoSourceDirectory,
initRepoAndPush,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import Sodium from 'libsodium-wrappers';
@@ -37,73 +30,6 @@ import {
} from './gitHelpers';
import { LoggerService } from '@backstage/backend-plugin-api';
const DEFAULT_TIMEOUT_MS = 60_000;
/**
* Helper for generating octokit configuration options for given repoUrl.
* If no token is provided, it will attempt to get a token from the credentials provider.
* @public
*/
export async function getOctokitOptions(options: {
integrations: ScmIntegrationRegistry;
credentialsProvider?: GithubCredentialsProvider;
token?: string;
repoUrl: string;
}): Promise<OctokitOptions> {
const { integrations, credentialsProvider, repoUrl, token } = options;
const { owner, repo, host } = parseRepoUrl(repoUrl, integrations);
const requestOptions = {
// set timeout to 60 seconds
timeout: DEFAULT_TIMEOUT_MS,
};
if (!owner) {
throw new InputError(`No owner provided for repo ${repoUrl}`);
}
const integrationConfig = integrations.github.byHost(host)?.config;
if (!integrationConfig) {
throw new InputError(`No integration for host ${host}`);
}
// short circuit the `githubCredentialsProvider` if there is a token provided by the caller already
if (token) {
return {
auth: token,
baseUrl: integrationConfig.apiBaseUrl,
previews: ['nebula-preview'],
request: requestOptions,
};
}
const githubCredentialsProvider =
credentialsProvider ??
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
// TODO(blam): Consider changing this API to take host and repo instead of repoUrl, as we end up parsing in this function
// and then parsing in the `getCredentials` function too the other side
const { token: credentialProviderToken } =
await githubCredentialsProvider.getCredentials({
url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent(
repo,
)}`,
});
if (!credentialProviderToken) {
throw new InputError(
`No token available for host: ${host}, with owner ${owner}, and repo ${repo}. Make sure GitHub auth is configured correctly. See https://backstage.io/docs/auth/github/provider for more details.`,
);
}
return {
auth: credentialProviderToken,
baseUrl: integrationConfig.apiBaseUrl,
previews: ['nebula-preview'],
};
}
export async function createGithubRepoWithCollaboratorsAndTopics(
client: Octokit,
repo: string,
@@ -29,5 +29,3 @@ export { createPublishGithubAction } from './github';
export { createGithubAutolinksAction } from './githubAutolinks';
export { createGithubPagesEnableAction } from './githubPagesEnable';
export { createGithubBranchProtectionAction } from './githubBranchProtection';
export { getOctokitOptions } from './helpers';
@@ -0,0 +1,83 @@
/*
* 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 { InputError } from '@backstage/errors';
import { createHandleAutocompleteRequest } from './autocomplete';
import { ScmIntegrationRegistry } from '@backstage/integration';
jest.mock('../util', () => {
return {
getOctokitOptions: jest.fn(),
};
});
const mockOctokit = {
paginate: async (fn: any) => (await fn()).data,
rest: {
repos: {
listForAuthenticatedUser: jest.fn(),
},
},
};
jest.mock('octokit', () => ({
Octokit: class {
constructor() {
return mockOctokit;
}
},
}));
describe('handleAutocompleteRequest', () => {
const mockIntegrations = {} as ScmIntegrationRegistry;
it('should return repositories with owner', async () => {
const handleAutocompleteRequest = createHandleAutocompleteRequest({
integrations: mockIntegrations,
});
mockOctokit.rest.repos.listForAuthenticatedUser.mockResolvedValue({
data: [
{
full_name: 'backstage/backstage',
},
],
});
const result = await handleAutocompleteRequest({
resource: 'repositoriesWithOwner',
token: 'token',
context: {},
});
expect(result).toEqual({
results: [{ id: 'backstage/backstage' }],
});
});
it('should throw an error for invalid resource', async () => {
const handleAutocompleteRequest = createHandleAutocompleteRequest({
integrations: mockIntegrations,
});
await expect(
handleAutocompleteRequest({
resource: 'invalid',
token: 'token',
context: {},
}),
).rejects.toThrow(InputError);
});
});
@@ -0,0 +1,56 @@
/*
* 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 { InputError } from '@backstage/errors';
import { getOctokitOptions } from '../util';
import { Octokit } from 'octokit';
import { ScmIntegrationRegistry } from '@backstage/integration';
export function createHandleAutocompleteRequest(options: {
integrations: ScmIntegrationRegistry;
}) {
return async function handleAutocompleteRequest({
resource,
token,
context,
}: {
resource: string;
token: string;
context: Record<string, string>;
}): Promise<{ results: { title?: string; id: string }[] }> {
const { integrations } = options;
const octokitOptions = await getOctokitOptions({
integrations,
token,
host: context.host ?? 'github.com',
});
const client = new Octokit(octokitOptions);
switch (resource) {
case 'repositoriesWithOwner': {
const repositoriesWithOwner = await client.paginate(
client.rest.repos.listForAuthenticatedUser,
);
const results = repositoriesWithOwner.map(r => ({ id: r.full_name }));
return { results };
}
default:
throw new InputError(`Invalid resource: ${resource}`);
}
};
}
@@ -22,3 +22,4 @@
export * from './actions';
export { githubModule as default } from './module';
export { getOctokitOptions } from './util';
@@ -17,7 +17,10 @@ import {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import {
scaffolderActionsExtensionPoint,
scaffolderAutocompleteExtensionPoint,
} from '@backstage/plugin-scaffolder-node/alpha';
import {
createGithubActionsDispatchAction,
createGithubAutolinksAction,
@@ -37,6 +40,7 @@ import {
ScmIntegrations,
} from '@backstage/integration';
import { CatalogClient } from '@backstage/catalog-client';
import { createHandleAutocompleteRequest } from './autocomplete/autocomplete';
/**
* @public
@@ -52,8 +56,9 @@ export const githubModule = createBackendModule({
config: coreServices.rootConfig,
discovery: coreServices.discovery,
auth: coreServices.auth,
autocomplete: scaffolderAutocompleteExtensionPoint,
},
async init({ scaffolder, config, discovery, auth }) {
async init({ scaffolder, config, discovery, auth, autocomplete }) {
const integrations = ScmIntegrations.fromConfig(config);
const githubCredentialsProvider =
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
@@ -109,6 +114,11 @@ export const githubModule = createBackendModule({
integrations,
}),
);
autocomplete.addAutocompleteProvider({
id: 'github',
handler: createHandleAutocompleteRequest({ integrations }),
});
},
});
},
@@ -0,0 +1,118 @@
/*
* Copyright 2025 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 { InputError } from '@backstage/errors';
import {
DefaultGithubCredentialsProvider,
GithubCredentialsProvider,
ScmIntegrationRegistry,
} from '@backstage/integration';
import { parseRepoUrl } from '@backstage/plugin-scaffolder-node';
import { OctokitOptions } from '@octokit/core/dist-types/types';
const DEFAULT_TIMEOUT_MS = 60_000;
/**
* Helper for generating octokit configuration options.
* If no token is provided, it will attempt to get a token from the credentials provider.
* @public
*/
export async function getOctokitOptions(options: {
integrations: ScmIntegrationRegistry;
credentialsProvider?: GithubCredentialsProvider;
token?: string;
host: string;
owner?: string;
repo?: string;
}): Promise<OctokitOptions>;
/**
* Helper for generating octokit configuration options for given repoUrl.
* If no token is provided, it will attempt to get a token from the credentials provider.
* @public
* @deprecated Use options `host`, `owner` and `repo` instead of `repoUrl`.
*/
export async function getOctokitOptions(options: {
integrations: ScmIntegrationRegistry;
credentialsProvider?: GithubCredentialsProvider;
token?: string;
repoUrl: string;
}): Promise<OctokitOptions>;
export async function getOctokitOptions(options: {
integrations: ScmIntegrationRegistry;
credentialsProvider?: GithubCredentialsProvider;
token?: string;
host?: string;
owner?: string;
repo?: string;
repoUrl?: string;
}): Promise<OctokitOptions> {
const { integrations, credentialsProvider, token, repoUrl } = options;
const { host, owner, repo } = repoUrl
? parseRepoUrl(repoUrl, integrations)
: options;
const requestOptions = {
// set timeout to 60 seconds
timeout: DEFAULT_TIMEOUT_MS,
};
const integrationConfig = integrations.github.byHost(host!)?.config;
if (!integrationConfig) {
throw new InputError(`No integration for host ${host}`);
}
// short circuit the `githubCredentialsProvider` if there is a token provided by the caller already
if (token) {
return {
auth: token,
baseUrl: integrationConfig.apiBaseUrl,
previews: ['nebula-preview'],
request: requestOptions,
};
}
if (!owner || !repo) {
throw new InputError(
`No owner and/or owner provided, which is required if a token is not provided`,
);
}
const githubCredentialsProvider =
credentialsProvider ??
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
const { token: credentialProviderToken } =
await githubCredentialsProvider.getCredentials({
url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent(
repo,
)}`,
});
if (!credentialProviderToken) {
throw new InputError(
`No token available for host: ${host}, with owner ${owner}, and repo ${repo}. Make sure GitHub auth is configured correctly. See https://backstage.io/docs/auth/github/provider for more details.`,
);
}
return {
auth: credentialProviderToken,
baseUrl: integrationConfig.apiBaseUrl,
previews: ['nebula-preview'],
};
}
+35 -25
View File
@@ -9,14 +9,27 @@ import { ActionContext as ActionContext_2 } from '@backstage/plugin-scaffolder-n
import { AuditorService } from '@backstage/backend-plugin-api';
import { AuthService } from '@backstage/backend-plugin-api';
import { AutocompleteHandler } from '@backstage/plugin-scaffolder-node/alpha';
import * as azure from '@backstage/plugin-scaffolder-backend-module-azure';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { BackstageCredentials } from '@backstage/backend-plugin-api';
import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucket';
import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud';
import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server';
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { createGithubActionsDispatchAction as createGithubActionsDispatchAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createGithubDeployKeyAction as createGithubDeployKeyAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createGithubEnvironmentAction as createGithubEnvironmentAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createGithubIssuesLabelAction as createGithubIssuesLabelAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { CreateGithubPullRequestActionOptions as CreateGithubPullRequestActionOptions_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createGithubRepoCreateAction as createGithubRepoCreateAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createGithubRepoPushAction as createGithubRepoPushAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createGithubWebhookAction as createGithubWebhookAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createPublishAzureAction as createPublishAzureAction_2 } from '@backstage/plugin-scaffolder-backend-module-azure';
import { createPublishBitbucketAction as createPublishBitbucketAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket';
import { createPublishBitbucketCloudAction as createPublishBitbucketCloudAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud';
import { createPublishBitbucketServerAction as createPublishBitbucketServerAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-server';
import { createPublishBitbucketServerPullRequestAction as createPublishBitbucketServerPullRequestAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-server';
import { createPublishGerritAction as createPublishGerritAction_2 } from '@backstage/plugin-scaffolder-backend-module-gerrit';
import { createPublishGerritReviewAction as createPublishGerritReviewAction_2 } from '@backstage/plugin-scaffolder-backend-module-gerrit';
import { createPublishGithubAction as createPublishGithubAction_2 } from '@backstage/plugin-scaffolder-backend-module-github';
import { createPublishGitlabAction as createPublishGitlabAction_2 } from '@backstage/plugin-scaffolder-backend-module-gitlab';
import { DatabaseService } from '@backstage/backend-plugin-api';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import { Duration } from 'luxon';
@@ -25,9 +38,6 @@ import { executeShellCommand as executeShellCommand_2 } from '@backstage/plugin-
import { ExecuteShellCommandOptions } from '@backstage/plugin-scaffolder-node';
import express from 'express';
import { fetchContents as fetchContents_2 } from '@backstage/plugin-scaffolder-node';
import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit';
import * as github from '@backstage/plugin-scaffolder-backend-module-github';
import * as gitlab from '@backstage/plugin-scaffolder-backend-module-gitlab';
import { HttpAuthService } from '@backstage/backend-plugin-api';
import { HumanDuration } from '@backstage/types';
import { IdentityApi } from '@backstage/plugin-auth-node';
@@ -265,57 +275,57 @@ export const createFilesystemRenameAction: () => TemplateAction_2<
>;
// @public @deprecated (undocumented)
export const createGithubActionsDispatchAction: typeof github.createGithubActionsDispatchAction;
export const createGithubActionsDispatchAction: typeof createGithubActionsDispatchAction_2;
// @public @deprecated (undocumented)
export const createGithubDeployKeyAction: typeof github.createGithubDeployKeyAction;
export const createGithubDeployKeyAction: typeof createGithubDeployKeyAction_2;
// @public @deprecated (undocumented)
export const createGithubEnvironmentAction: typeof github.createGithubEnvironmentAction;
export const createGithubEnvironmentAction: typeof createGithubEnvironmentAction_2;
// @public @deprecated (undocumented)
export const createGithubIssuesLabelAction: typeof github.createGithubIssuesLabelAction;
export const createGithubIssuesLabelAction: typeof createGithubIssuesLabelAction_2;
// @public @deprecated (undocumented)
export type CreateGithubPullRequestActionOptions =
github.CreateGithubPullRequestActionOptions;
CreateGithubPullRequestActionOptions_2;
// @public @deprecated (undocumented)
export const createGithubRepoCreateAction: typeof github.createGithubRepoCreateAction;
export const createGithubRepoCreateAction: typeof createGithubRepoCreateAction_2;
// @public @deprecated (undocumented)
export const createGithubRepoPushAction: typeof github.createGithubRepoPushAction;
export const createGithubRepoPushAction: typeof createGithubRepoPushAction_2;
// @public @deprecated (undocumented)
export const createGithubWebhookAction: typeof github.createGithubWebhookAction;
export const createGithubWebhookAction: typeof createGithubWebhookAction_2;
// @public @deprecated (undocumented)
export const createPublishAzureAction: typeof azure.createPublishAzureAction;
export const createPublishAzureAction: typeof createPublishAzureAction_2;
// @public @deprecated (undocumented)
export const createPublishBitbucketAction: typeof bitbucket.createPublishBitbucketAction;
export const createPublishBitbucketAction: typeof createPublishBitbucketAction_2;
// @public @deprecated (undocumented)
export const createPublishBitbucketCloudAction: typeof bitbucketCloud.createPublishBitbucketCloudAction;
export const createPublishBitbucketCloudAction: typeof createPublishBitbucketCloudAction_2;
// @public @deprecated (undocumented)
export const createPublishBitbucketServerAction: typeof bitbucketServer.createPublishBitbucketServerAction;
export const createPublishBitbucketServerAction: typeof createPublishBitbucketServerAction_2;
// @public @deprecated (undocumented)
export const createPublishBitbucketServerPullRequestAction: typeof bitbucketServer.createPublishBitbucketServerPullRequestAction;
export const createPublishBitbucketServerPullRequestAction: typeof createPublishBitbucketServerPullRequestAction_2;
// @public @deprecated (undocumented)
export const createPublishGerritAction: typeof gerrit.createPublishGerritAction;
export const createPublishGerritAction: typeof createPublishGerritAction_2;
// @public @deprecated (undocumented)
export const createPublishGerritReviewAction: typeof gerrit.createPublishGerritReviewAction;
export const createPublishGerritReviewAction: typeof createPublishGerritReviewAction_2;
// @public @deprecated (undocumented)
export const createPublishGithubAction: typeof github.createPublishGithubAction;
export const createPublishGithubAction: typeof createPublishGithubAction_2;
// @public @deprecated (undocumented)
export const createPublishGithubPullRequestAction: (
options: github.CreateGithubPullRequestActionOptions,
options: CreateGithubPullRequestActionOptions_2,
) => TemplateAction_2<
{
title: string;
@@ -340,7 +350,7 @@ export const createPublishGithubPullRequestAction: (
>;
// @public @deprecated (undocumented)
export const createPublishGitlabAction: typeof gitlab.createPublishGitlabAction;
export const createPublishGitlabAction: typeof createPublishGitlabAction_2;
// @public @deprecated (undocumented)
export const createPublishGitlabMergeRequestAction: (options: {
@@ -13,134 +13,151 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as github from '@backstage/plugin-scaffolder-backend-module-github';
import * as gitlab from '@backstage/plugin-scaffolder-backend-module-gitlab';
import * as azure from '@backstage/plugin-scaffolder-backend-module-azure';
import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucket';
import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud';
import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server';
import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit';
import {
createGithubActionsDispatchAction as githubActionsDispatch,
createGithubDeployKeyAction as githubDeployKey,
createGithubEnvironmentAction as githubEnvironment,
createGithubIssuesLabelAction as githubIssuesLabel,
CreateGithubPullRequestActionOptions as GithubPullRequestActionOptions,
createGithubRepoCreateAction as githubRepoCreate,
createGithubRepoPushAction as githubRepoPush,
createGithubWebhookAction as githubWebhook,
createPublishGithubAction as publishGithub,
createPublishGithubPullRequestAction as publishGithubPullRequest,
} from '@backstage/plugin-scaffolder-backend-module-github';
import {
createPublishGitlabAction as publishGitlab,
createPublishGitlabMergeRequestAction as publishGitlabMergeRequest,
} from '@backstage/plugin-scaffolder-backend-module-gitlab';
import { createPublishAzureAction as publishAzure } from '@backstage/plugin-scaffolder-backend-module-azure';
import { createPublishBitbucketAction as publishBitbucket } from '@backstage/plugin-scaffolder-backend-module-bitbucket';
import { createPublishBitbucketCloudAction as publishBitbucketCloud } from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud';
import {
createPublishBitbucketServerAction as publishBitbucketServer,
createPublishBitbucketServerPullRequestAction as publishBitbucketServerPullRequest,
} from '@backstage/plugin-scaffolder-backend-module-bitbucket-server';
import {
createPublishGerritAction as publishGerrit,
createPublishGerritReviewAction as publishGerritReview,
} from '@backstage/plugin-scaffolder-backend-module-gerrit';
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export const createGithubActionsDispatchAction =
github.createGithubActionsDispatchAction;
export const createGithubActionsDispatchAction = githubActionsDispatch;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export const createGithubDeployKeyAction = github.createGithubDeployKeyAction;
export const createGithubDeployKeyAction = githubDeployKey;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export const createGithubEnvironmentAction =
github.createGithubEnvironmentAction;
export const createGithubEnvironmentAction = githubEnvironment;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export const createGithubIssuesLabelAction =
github.createGithubIssuesLabelAction;
export const createGithubIssuesLabelAction = githubIssuesLabel;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export type CreateGithubPullRequestActionOptions =
github.CreateGithubPullRequestActionOptions;
GithubPullRequestActionOptions;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export const createGithubRepoCreateAction = github.createGithubRepoCreateAction;
export const createGithubRepoCreateAction = githubRepoCreate;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export const createGithubRepoPushAction = github.createGithubRepoPushAction;
export const createGithubRepoPushAction = githubRepoPush;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export const createGithubWebhookAction = github.createGithubWebhookAction;
export const createGithubWebhookAction = githubWebhook;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export const createPublishGithubAction = github.createPublishGithubAction;
export const createPublishGithubAction = publishGithub;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead
*/
export const createPublishGithubPullRequestAction =
github.createPublishGithubPullRequestAction;
export const createPublishGithubPullRequestAction = publishGithubPullRequest;
/**
* @public @deprecated use "createPublishBitbucketCloudAction" from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud or "createPublishBitbucketServerAction" from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead
*/
export const createPublishBitbucketAction =
bitbucket.createPublishBitbucketAction;
export const createPublishBitbucketAction = publishBitbucket;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead
*/
export const createPublishBitbucketCloudAction =
bitbucketCloud.createPublishBitbucketCloudAction;
export const createPublishBitbucketCloudAction = publishBitbucketCloud;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead
*/
export const createPublishBitbucketServerAction =
bitbucketServer.createPublishBitbucketServerAction;
export const createPublishBitbucketServerAction = publishBitbucketServer;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead
*/
export const createPublishBitbucketServerPullRequestAction =
bitbucketServer.createPublishBitbucketServerPullRequestAction;
publishBitbucketServerPullRequest;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-azure` instead
*/
export const createPublishAzureAction = azure.createPublishAzureAction;
export const createPublishAzureAction = publishAzure;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead
*/
export const createPublishGerritAction = gerrit.createPublishGerritAction;
export const createPublishGerritAction = publishGerrit;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead
*/
export const createPublishGerritReviewAction =
gerrit.createPublishGerritReviewAction;
export const createPublishGerritReviewAction = publishGerritReview;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead
*/
export const createPublishGitlabAction = gitlab.createPublishGitlabAction;
export const createPublishGitlabAction = publishGitlab;
/**
* @public
* @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead
*/
export const createPublishGitlabMergeRequestAction =
gitlab.createPublishGitlabMergeRequestAction;
export const createPublishGitlabMergeRequestAction = publishGitlabMergeRequest;
@@ -14,22 +14,43 @@
* limitations under the License.
*/
import React from 'react';
import React, { act } from 'react';
import { GithubRepoPicker } from './GithubRepoPicker';
import { fireEvent } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import { fireEvent, waitFor } from '@testing-library/react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import {
ScaffolderApi,
scaffolderApiRef,
} from '@backstage/plugin-scaffolder-react';
import userEvent from '@testing-library/user-event';
describe('GithubRepoPicker', () => {
const scaffolderApiMock: Partial<ScaffolderApi> = {
autocomplete: jest.fn().mockImplementation(opts =>
Promise.resolve({
results: [
{
id:
opts.resource === 'repositoriesWithOwner'
? 'spotify/backstage'
: `${opts.resource}_example`,
},
],
}),
),
};
describe('owner field', () => {
it('renders a select if there is a list of allowed owners', async () => {
const allowedOwners = ['owner1', 'owner2'];
const { findByText } = await renderInTestApp(
<GithubRepoPicker
onChange={jest.fn()}
rawErrors={[]}
state={{ repoName: 'repo' }}
allowedOwners={allowedOwners}
/>,
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<GithubRepoPicker
onChange={jest.fn()}
rawErrors={[]}
state={{ repoName: 'repo' }}
allowedOwners={allowedOwners}
/>
</TestApiProvider>,
);
expect(await findByText('owner1')).toBeInTheDocument();
@@ -40,12 +61,14 @@ describe('GithubRepoPicker', () => {
const onChange = jest.fn();
const allowedOwners = ['owner1', 'owner2'];
const { getByRole } = await renderInTestApp(
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
state={{ repoName: 'repo' }}
allowedOwners={allowedOwners}
/>,
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
state={{ repoName: 'repo' }}
allowedOwners={allowedOwners}
/>
</TestApiProvider>,
);
await fireEvent.change(getByRole('combobox'), {
@@ -59,12 +82,14 @@ describe('GithubRepoPicker', () => {
const onChange = jest.fn();
const allowedOwners = ['owner1'];
const { getByRole } = await renderInTestApp(
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
state={{ repoName: 'repo' }}
allowedOwners={allowedOwners}
/>,
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
state={{ repoName: 'repo' }}
allowedOwners={allowedOwners}
/>
</TestApiProvider>,
);
expect(getByRole('combobox')).toBeDisabled();
@@ -73,16 +98,76 @@ describe('GithubRepoPicker', () => {
it('should display free text if no allowed owners are passed', async () => {
const onChange = jest.fn();
const { getAllByRole } = await renderInTestApp(
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
state={{ repoName: 'repo' }}
/>,
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
state={{ repoName: 'repo' }}
/>
</TestApiProvider>,
);
const ownerField = getAllByRole('textbox')[0];
fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } });
act(() => {
ownerField.focus();
fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } });
ownerField.blur();
});
expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' });
});
});
describe('autocompletion', () => {
it('should populate owners if accessToken is provided', async () => {
const onChange = jest.fn();
const { getAllByRole, getByText } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
state={{ host: 'github.com', repoName: 'repo' }}
accessToken="foo"
/>
</TestApiProvider>,
);
// Open the Autcomplete dropdown
const ownerInput = getAllByRole('textbox')[0];
await userEvent.click(ownerInput);
// Verify that the available owners are shown
await waitFor(() => expect(getByText('spotify')).toBeInTheDocument());
// Verify that selecting an option calls onChange
await userEvent.click(getByText('spotify'));
expect(onChange).toHaveBeenCalledWith({
owner: 'spotify',
});
});
it('should populate repositories if owner and accessToken are provided', async () => {
const onChange = jest.fn();
await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
<GithubRepoPicker
onChange={onChange}
rawErrors={[]}
state={{ host: 'github.com', owner: 'spotify' }}
accessToken="foo"
/>
</TestApiProvider>,
);
// Verify that the available repos are updated
await waitFor(
() =>
expect(onChange).toHaveBeenCalledWith({
availableRepos: [{ name: 'backstage' }],
}),
{ timeout: 1500 },
);
});
});
});
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import TextField from '@material-ui/core/TextField';
@@ -21,19 +21,79 @@ import { Select, SelectItem } from '@backstage/core-components';
import { BaseRepoUrlPickerProps } from './types';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
import { useApi } from '@backstage/core-plugin-api';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import useDebounce from 'react-use/esm/useDebounce';
import Autocomplete from '@material-ui/lab/Autocomplete';
import uniq from 'lodash/uniq';
import map from 'lodash/map';
export const GithubRepoPicker = (
props: BaseRepoUrlPickerProps<{
allowedOwners?: string[];
accessToken?: string;
}>,
) => {
const { allowedOwners = [], rawErrors, state, onChange } = props;
const { allowedOwners = [], rawErrors, state, onChange, accessToken } = props;
const { t } = useTranslationRef(scaffolderTranslationRef);
const ownerItems: SelectItem[] = allowedOwners
? allowedOwners.map(i => ({ label: i, value: i }))
: [{ label: 'Loading...', value: 'loading' }];
const { owner } = state;
const { host, owner } = state;
const scaffolderApi = useApi(scaffolderApiRef);
const [availableRepositoriesWithOwner, setAvailableRepositoriesWithOwner] =
useState<{ owner: string; name: string }[]>([]);
// Update available repositories with owner when client is available
const updateAvailableRepositoriesWithOwner = useCallback(() => {
if (!scaffolderApi.autocomplete || !accessToken || !host) {
setAvailableRepositoriesWithOwner([]);
return;
}
scaffolderApi
.autocomplete({
token: accessToken,
resource: 'repositoriesWithOwner',
provider: 'github',
context: { host },
})
.then(({ results }) => {
setAvailableRepositoriesWithOwner(
results.map(r => {
const [rOwner, rName] = r.id.split('/');
return { owner: rOwner, name: rName };
}),
);
})
.catch(() => {
setAvailableRepositoriesWithOwner([]);
});
}, [scaffolderApi, accessToken, host]);
useDebounce(updateAvailableRepositoriesWithOwner, 500, [
updateAvailableRepositoriesWithOwner,
]);
// Update available owners when available repositories with owner change
const availableOwners = useMemo<string[]>(
() => uniq(map(availableRepositoriesWithOwner, 'owner')),
[availableRepositoriesWithOwner],
);
// Update available repositories when available repositories with owner change or when owner changes
const updateAvailableRepositories = useCallback(() => {
const availableRepos = availableRepositoriesWithOwner.flatMap(r =>
r.owner === owner ? [{ name: r.name }] : [],
);
onChange({ availableRepos });
}, [availableRepositoriesWithOwner, owner, onChange]);
useDebounce(updateAvailableRepositories, 500, [updateAvailableRepositories]);
return (
<>
@@ -54,19 +114,28 @@ export const GithubRepoPicker = (
selected={owner}
items={ownerItems}
/>
<FormHelperText>
{t('fields.githubRepoPicker.owner.description')}
</FormHelperText>
</>
) : (
<TextField
id="ownerInput"
label={t('fields.githubRepoPicker.owner.inputTitle')}
onChange={e => onChange({ owner: e.target.value })}
helperText={t('fields.githubRepoPicker.owner.description')}
<Autocomplete
value={owner}
onChange={(_, newValue) => {
onChange({ owner: newValue || '' });
}}
options={availableOwners}
renderInput={params => (
<TextField
{...params}
label={t('fields.githubRepoPicker.owner.inputTitle')}
required
/>
)}
freeSolo
autoSelect
/>
)}
<FormHelperText>
{t('fields.githubRepoPicker.owner.description')}
</FormHelperText>
</FormControl>
</>
);
@@ -186,6 +186,10 @@ export const RepoUrlPicker = (
onChange={updateLocalState}
rawErrors={rawErrors}
state={state}
accessToken={
uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey &&
secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey]
}
/>
)}
{hostType === 'gitea' && (