feat(scaffolder): add support for autocompleting GitHub owners to scaffolder
Signed-off-by: Benjamin Janssens <benji.janssens@gmail.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 'owners': {
|
||||
const organizations = await client.paginate(
|
||||
client.rest.orgs.listForAuthenticatedUser,
|
||||
);
|
||||
|
||||
const results = organizations.map(organization => ({
|
||||
id: organization.login,
|
||||
}));
|
||||
|
||||
return { results };
|
||||
}
|
||||
default:
|
||||
throw new InputError(`Invalid resource: ${resource}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 }),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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, 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,13 +21,18 @@ 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';
|
||||
|
||||
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 }))
|
||||
@@ -35,6 +40,33 @@ export const GithubRepoPicker = (
|
||||
|
||||
const { owner } = state;
|
||||
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
|
||||
const [availableOwners, setAvailableOwners] = useState<string[]>([]);
|
||||
|
||||
// Update available owners when client is available
|
||||
const updateAvailableOwners = useCallback(() => {
|
||||
if (!scaffolderApi.autocomplete || !accessToken) {
|
||||
setAvailableOwners([]);
|
||||
return;
|
||||
}
|
||||
|
||||
scaffolderApi
|
||||
.autocomplete({
|
||||
token: accessToken,
|
||||
resource: 'owners',
|
||||
provider: 'github',
|
||||
})
|
||||
.then(({ results }) => {
|
||||
setAvailableOwners(results.map(r => r.id));
|
||||
})
|
||||
.catch(() => {
|
||||
setAvailableOwners([]);
|
||||
});
|
||||
}, [scaffolderApi, accessToken]);
|
||||
|
||||
useDebounce(updateAvailableOwners, 500, [updateAvailableOwners]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
@@ -54,19 +86,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' && (
|
||||
|
||||
Reference in New Issue
Block a user