From be6de37e4d93dd2b8d97b3399ac1da605b86afea Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Fri, 25 Oct 2024 16:04:51 +0200 Subject: [PATCH 1/8] Add autocomplete to GitlabRepoUrlPicker Signed-off-by: Severin Wischmann --- .../src/autocomplete/autocomplete.ts | 23 ++-- .../src/autocomplete/autocomplete.ts | 108 ++++++++++++++++++ .../src/module.ts | 14 ++- plugins/scaffolder-node/src/alpha.ts | 4 +- plugins/scaffolder-react/src/api/types.ts | 2 +- .../RepoUrlPicker/BitbucketRepoPicker.tsx | 14 +-- .../RepoUrlPicker/GitlabRepoPicker.test.tsx | 78 ++++++++----- .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 100 ++++++++++++++-- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 32 +++--- 9 files changed, 305 insertions(+), 70 deletions(-) create mode 100644 plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 75cc170a30..542e58574e 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -25,7 +25,7 @@ export async function handleAutocompleteRequest({ resource: string; token: string; context: Record; -}): Promise<{ results: { title: string }[] }> { +}): Promise<{ results: { title: string; id?: string }[] }> { const client = BitbucketCloudClient.fromConfig({ host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0', @@ -34,10 +34,13 @@ export async function handleAutocompleteRequest({ switch (resource) { case 'workspaces': { - const results: { title: string }[] = []; + const results: { title: string; id: string }[] = []; for await (const page of client.listWorkspaces().iteratePages()) { - const slugs = [...page.values!].map(p => ({ title: p.slug! })); + const slugs = [...page.values!].map(p => ({ + title: p.slug!, + id: p.uuid!, + })); results.push(...slugs); } @@ -47,12 +50,15 @@ export async function handleAutocompleteRequest({ if (!context.workspace) throw new InputError('Missing workspace context parameter'); - const results: { title: string }[] = []; + const results: { title: string; id: string }[] = []; for await (const page of client .listProjectsByWorkspace(context.workspace) .iteratePages()) { - const keys = [...page.values!].map(p => ({ title: p.key! })); + const keys = [...page.values!].map(p => ({ + title: p.key!, + id: p.uuid!, + })); results.push(...keys); } @@ -64,14 +70,17 @@ export async function handleAutocompleteRequest({ 'Missing workspace and/or project context parameter', ); - const results: { title: string }[] = []; + const results: { title: string; id: string }[] = []; for await (const page of client .listRepositoriesByWorkspace(context.workspace, { q: `project.key="${context.project}"`, }) .iteratePages()) { - const slugs = [...page.values!].map(p => ({ title: p.slug! })); + const slugs = [...page.values!].map(p => ({ + title: p.slug!, + id: p.uuid!, + })); results.push(...slugs); } diff --git a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts new file mode 100644 index 0000000000..2a15c0c99e --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts @@ -0,0 +1,108 @@ +/* + * 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 { ScmIntegrationRegistry } from '@backstage/integration'; +import { getClient } from '../util'; + +export function createHandleAutocompleteRequest(options: { + integrations: ScmIntegrationRegistry; +}) { + return async function handleAutocompleteRequest({ + resource, + token, + context, + }: { + resource: string; + token: string; + context: Record; + }): Promise<{ + results: { + title: string; + id?: string; + }[]; + }> { + const { integrations } = options; + const client = getClient({ + host: context.host ?? 'gitlab.com', + integrations, + token, + }); + + switch (resource) { + case 'groups': { + let groups: any[] = []; + let page = 1; + const perPage = 100; + let response = []; + let continueFetch = true; + while (continueFetch) { + response = await client.Groups.all({ + pagination: 'offset', + page, + perPage, + }); + + groups = groups.concat(response); + if (response.length < perPage) continueFetch = false; + page++; + } + + const result: { + results: { + title: string; + id: string; + }[]; + } = { + results: groups.map(group => ({ + title: group.full_path, + id: group.id.toString(), + })), + }; + // append also user context + const user = await client.Users.showCurrentUser(); + result.results.push({ + title: user.username, + id: user.id.toString(), + }); + + return result; + } + case 'repositories': { + if (!context.id) + throw new InputError('Missing groupId and userId context parameter'); + + let response; + if ( + context.id === (await client.Users.showCurrentUser())?.id.toString() + ) { + response = await client.Users.allProjects(context.id); + } else { + response = await client.Groups.allProjects(context.id); + } + + return { + results: response.map(project => ({ + title: project.name.trim(), + id: project.id.toString(), + })), + }; + } + default: + throw new InputError(`Invalid resource: ${resource}`); + } + }; +} diff --git a/plugins/scaffolder-backend-module-gitlab/src/module.ts b/plugins/scaffolder-backend-module-gitlab/src/module.ts index 78175532ed..9f18532cdf 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/module.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/module.ts @@ -18,7 +18,10 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { ScmIntegrations } from '@backstage/integration'; -import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; +import { + scaffolderActionsExtensionPoint, + scaffolderAutocompleteExtensionPoint, +} from '@backstage/plugin-scaffolder-node/alpha'; import { createGitlabGroupEnsureExistsAction, createGitlabIssueAction, @@ -31,6 +34,7 @@ import { createTriggerGitlabPipelineAction, editGitlabIssueAction, } from './actions'; +import { createHandleAutocompleteRequest } from './autocomplete/autocomplete'; /** * @public @@ -43,9 +47,10 @@ export const gitlabModule = createBackendModule({ registerInit({ deps: { scaffolder: scaffolderActionsExtensionPoint, + autocomplete: scaffolderAutocompleteExtensionPoint, config: coreServices.rootConfig, }, - async init({ scaffolder, config }) { + async init({ scaffolder, autocomplete, config }) { const integrations = ScmIntegrations.fromConfig(config); scaffolder.addActions( @@ -60,6 +65,11 @@ export const gitlabModule = createBackendModule({ createPublishGitlabMergeRequestAction({ integrations }), createTriggerGitlabPipelineAction({ integrations }), ); + + autocomplete.addAutocompleteProvider({ + id: 'gitlab', + handler: createHandleAutocompleteRequest({ integrations }), + }); }, }); }, diff --git a/plugins/scaffolder-node/src/alpha.ts b/plugins/scaffolder-node/src/alpha.ts index 6a0e34faf7..a8b7bde76b 100644 --- a/plugins/scaffolder-node/src/alpha.ts +++ b/plugins/scaffolder-node/src/alpha.ts @@ -16,10 +16,10 @@ import { createExtensionPoint } from '@backstage/backend-plugin-api'; import { + TaskBroker, TemplateAction, TemplateFilter, TemplateGlobal, - TaskBroker, } from '@backstage/plugin-scaffolder-node'; export * from './tasks/alpha'; @@ -95,7 +95,7 @@ export type AutocompleteHandler = ({ resource: string; token: string; context: Record; -}) => Promise<{ results: { title: string }[] }>; +}) => Promise<{ results: { title: string; id?: string }[] }>; /** * Extension point for adding autocomplete handler providers diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index bc37eed47e..73dd6c50d2 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -245,5 +245,5 @@ export interface ScaffolderApi { provider: string; resource: string; context?: Record; - }): Promise<{ results: { title: string }[] }>; + }): Promise<{ results: { title: string; id?: string }[] }>; } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index 28cd0cf5bc..c2c6b7f0d6 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import { Select, SelectItem } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; -import { Select, SelectItem } from '@backstage/core-components'; -import { BaseRepoUrlPickerProps } from './types'; -import Autocomplete from '@material-ui/lab/Autocomplete'; import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import React, { useCallback, useEffect, useState } from 'react'; import useDebounce from 'react-use/esm/useDebounce'; -import { useApi } from '@backstage/core-plugin-api'; -import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { scaffolderTranslationRef } from '../../../translation'; +import { BaseRepoUrlPickerProps } from './types'; /** * The underlying component that is rendered in the form for the `BitbucketRepoPicker` diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx index f3d6e2da91..a8fb42cae3 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.test.tsx @@ -14,22 +14,36 @@ * limitations under the License. */ +import { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; import React from 'react'; import { GitlabRepoPicker } from './GitlabRepoPicker'; -import { fireEvent } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; describe('GitlabRepoPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest.fn().mockImplementation(opts => + Promise.resolve({ + results: [{ title: `${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( - , + + + , + , ); expect(await findByText('owner1')).toBeInTheDocument(); @@ -40,12 +54,15 @@ describe('GitlabRepoPicker', () => { const onChange = jest.fn(); const allowedOwners = ['owner1', 'owner2']; const { getByRole } = await renderInTestApp( - , + + + , + , ); await fireEvent.change(getByRole('combobox'), { @@ -59,12 +76,15 @@ describe('GitlabRepoPicker', () => { const onChange = jest.fn(); const allowedOwners = ['owner1']; const { getByRole } = await renderInTestApp( - , + + + , + , ); expect(getByRole('combobox')).toBeDisabled(); @@ -73,14 +93,20 @@ describe('GitlabRepoPicker', () => { it('should display free text if no allowed owners are passed', async () => { const onChange = jest.fn(); const { getAllByRole } = await renderInTestApp( - , + + + , ); const ownerField = getAllByRole('textbox')[0]; - fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } }); + ownerField.focus(); + fireEvent.change(ownerField, { + target: { value: 'my-mock-owner' }, + }); + ownerField.blur(); expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index c0879008a1..8ac7be0e97 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -13,28 +13,97 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import { Select, SelectItem } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import TextField from '@material-ui/core/TextField'; -import { Select, SelectItem } from '@backstage/core-components'; -import { BaseRepoUrlPickerProps } from './types'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import React, { useCallback, useState } from 'react'; +import useDebounce from 'react-use/esm/useDebounce'; import { scaffolderTranslationRef } from '../../../translation'; +import { BaseRepoUrlPickerProps } from './types'; export const GitlabRepoPicker = ( props: BaseRepoUrlPickerProps<{ allowedOwners?: string[]; allowedRepos?: string[]; + accessToken?: string; }>, ) => { - const { allowedOwners = [], state, onChange, rawErrors } = props; + const { allowedOwners = [], state, onChange, rawErrors, accessToken } = props; + const [availableGroups, setAvailableGroups] = useState< + { title: string; id: string }[] + >([]); const { t } = useTranslationRef(scaffolderTranslationRef); const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; - const { owner } = state; + const { owner, host } = state; + + const scaffolderApi = useApi(scaffolderApiRef); + + const updateAvailableGroups = useCallback(() => { + if (!scaffolderApi.autocomplete || !accessToken || !host) { + setAvailableGroups([]); + return; + } + + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'groups', + provider: 'gitlab', + context: { host }, + }) + .then(({ results }) => { + setAvailableGroups( + results.map(r => { + return { + title: r.title, + id: r.id!, + }; + }), + ); + }) + .catch(() => { + setAvailableGroups([]); + }); + }, [scaffolderApi, accessToken, host]); + + useDebounce(updateAvailableGroups, 500, [updateAvailableGroups]); + + // Update available repositories when client is available and group changes + const updateAvailableRepositories = useCallback(() => { + if (!scaffolderApi.autocomplete || !accessToken || !host || !owner) { + onChange({ availableRepos: [] }); + return; + } + + const selectedGroup = availableGroups.find(group => group.title === owner); + + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'repositories', + context: { + id: selectedGroup?.id ?? '', + host, + }, + provider: 'gitlab', + }) + .then(({ results }) => { + onChange({ availableRepos: results.map(r => r.title) }); + }) + .catch(() => { + onChange({ availableRepos: [] }); + }); + }, [scaffolderApi, accessToken, host, owner, onChange, availableGroups]); + + useDebounce(updateAvailableRepositories, 500, [updateAvailableRepositories]); return ( <> @@ -64,12 +133,21 @@ export const GitlabRepoPicker = ( ) : ( - onChange({ owner: e.target.value })} - helperText={t('fields.gitlabRepoPicker.owner.description')} + { + onChange({ owner: newValue || '' }); + }} + options={availableGroups.map(group => group.title)} + renderInput={params => ( + + )} + freeSolo + autoSelect /> )} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 3c9ca9c2ca..c0e52e1826 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -15,26 +15,26 @@ */ import { useApi } from '@backstage/core-plugin-api'; import { - scmIntegrationsApiRef, scmAuthApiRef, + scmIntegrationsApiRef, } from '@backstage/integration-react'; -import React, { useEffect, useState, useMemo, useCallback } from 'react'; -import { GithubRepoPicker } from './GithubRepoPicker'; -import { GiteaRepoPicker } from './GiteaRepoPicker'; -import { GitlabRepoPicker } from './GitlabRepoPicker'; -import { AzureRepoPicker } from './AzureRepoPicker'; -import { BitbucketRepoPicker } from './BitbucketRepoPicker'; -import { GerritRepoPicker } from './GerritRepoPicker'; -import { RepoUrlPickerHost } from './RepoUrlPickerHost'; -import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; -import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; -import { RepoUrlPickerFieldSchema } from './schema'; -import { RepoUrlPickerState } 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 React, { useCallback, useEffect, useMemo, useState } from 'react'; +import useDebounce from 'react-use/esm/useDebounce'; +import { AzureRepoPicker } from './AzureRepoPicker'; +import { BitbucketRepoPicker } from './BitbucketRepoPicker'; +import { GerritRepoPicker } from './GerritRepoPicker'; +import { GiteaRepoPicker } from './GiteaRepoPicker'; +import { GithubRepoPicker } from './GithubRepoPicker'; +import { GitlabRepoPicker } from './GitlabRepoPicker'; +import { RepoUrlPickerHost } from './RepoUrlPickerHost'; +import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; +import { RepoUrlPickerFieldSchema } from './schema'; +import { RepoUrlPickerState } from './types'; +import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; export { RepoUrlPickerSchema } from './schema'; @@ -203,6 +203,10 @@ export const RepoUrlPicker = ( rawErrors={rawErrors} state={state} onChange={updateLocalState} + accessToken={ + uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey && + secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey] + } /> )} {hostType === 'bitbucket' && ( From c4ffd1326052e88410ec3b525f6163273d9b31eb Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Fri, 25 Oct 2024 16:09:43 +0200 Subject: [PATCH 2/8] Added changeset Signed-off-by: Severin Wischmann --- .changeset/khaki-shrimps-tease.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/khaki-shrimps-tease.md diff --git a/.changeset/khaki-shrimps-tease.md b/.changeset/khaki-shrimps-tease.md new file mode 100644 index 0000000000..a4dd3bc8e0 --- /dev/null +++ b/.changeset/khaki-shrimps-tease.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': minor +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder-node': patch +'@backstage/plugin-scaffolder': patch +--- + +Added the autocomplete feature to GitlabRepoUrlPicker From 42022d677870b3d76e3a9703474147fa21983f1e Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Sat, 26 Oct 2024 00:49:43 +0200 Subject: [PATCH 3/8] Adding API report Signed-off-by: Severin Wischmann --- plugins/scaffolder-node/report-alpha.api.md | 1 + plugins/scaffolder-react/report.api.md | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/scaffolder-node/report-alpha.api.md b/plugins/scaffolder-node/report-alpha.api.md index c9b8130f0e..4ecbc71358 100644 --- a/plugins/scaffolder-node/report-alpha.api.md +++ b/plugins/scaffolder-node/report-alpha.api.md @@ -23,6 +23,7 @@ export type AutocompleteHandler = ({ }) => Promise<{ results: { title: string; + id?: string; }[]; }>; diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index 5523b52ad9..887ccc7cb4 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -215,6 +215,7 @@ export interface ScaffolderApi { }): Promise<{ results: { title: string; + id?: string; }[]; }>; cancelTask(taskId: string): Promise; From 38682faf86d39759a4b8262129ac57be390b169f Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Wed, 6 Nov 2024 16:14:06 +0100 Subject: [PATCH 4/8] Make id mandatory and title optional Signed-off-by: Severin Wischmann --- .../src/autocomplete/autocomplete.test.ts | 58 +++++-- .../src/autocomplete/autocomplete.ts | 9 +- .../src/autocomplete/autocomplete.test.ts | 152 ++++++++++++++++++ .../src/autocomplete/autocomplete.ts | 4 +- plugins/scaffolder-node/src/alpha.ts | 2 +- plugins/scaffolder-react/report.api.md | 4 +- plugins/scaffolder-react/src/api/types.ts | 2 +- plugins/scaffolder/report.api.md | 3 +- plugins/scaffolder/src/api.ts | 9 +- .../BitbucketRepoBranchPicker.tsx | 8 +- .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 6 +- .../components/fields/RepoUrlPicker/types.ts | 1 + .../fields/RepoUrlPicker/utils.test.ts | 6 +- .../components/fields/RepoUrlPicker/utils.ts | 7 +- 14 files changed, 232 insertions(+), 39 deletions(-) create mode 100644 plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts index d99a8358b9..1729593d2b 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts @@ -14,26 +14,44 @@ * limitations under the License. */ +import { InputError } from '@backstage/errors'; import { BitbucketCloudClient } from '@backstage/plugin-bitbucket-cloud-common'; import { handleAutocompleteRequest } from './autocomplete'; -import { InputError } from '@backstage/errors'; describe('handleAutocompleteRequest', () => { const client: Partial = { listWorkspaces: jest.fn().mockReturnValue({ - iteratePages: jest - .fn() - .mockReturnValue([{ values: [{ slug: 'workspace1' }] }]), + iteratePages: jest.fn().mockReturnValue([ + { + values: [ + { + slug: 'workspace1', + uuid: '486F4F29-9B88-4BE8-9092-24BEB8A5F3B3', + }, + ], + }, + ]), }), listProjectsByWorkspace: jest.fn().mockReturnValue({ - iteratePages: jest - .fn() - .mockReturnValue([{ values: [{ key: 'project1' }] }]), + iteratePages: jest.fn().mockReturnValue([ + { + values: [ + { key: 'project1', uuid: '70F065E3-CE7C-487A-99B6-D81EB84E5A21' }, + ], + }, + ]), }), listRepositoriesByWorkspace: jest.fn().mockReturnValue({ - iteratePages: jest - .fn() - .mockReturnValue([{ values: [{ slug: 'repository1' }] }]), + iteratePages: jest.fn().mockReturnValue([ + { + values: [ + { + slug: 'repository1', + uuid: 'F2F0DAF7-B4D6-4694-A131-C2478A200AC5', + }, + ], + }, + ]), }), listBranchesByRepository: jest.fn().mockReturnValue({ iteratePages: jest @@ -66,7 +84,11 @@ describe('handleAutocompleteRequest', () => { resource: 'workspaces', }); - expect(result).toEqual({ results: [{ title: 'workspace1' }] }); + expect(result).toEqual({ + results: [ + { title: 'workspace1', id: '486F4F29-9B88-4BE8-9092-24BEB8A5F3B3' }, + ], + }); }); it('should return projects', async () => { @@ -78,7 +100,11 @@ describe('handleAutocompleteRequest', () => { resource: 'projects', }); - expect(result).toEqual({ results: [{ title: 'project1' }] }); + expect(result).toEqual({ + results: [ + { title: 'project1', id: '70F065E3-CE7C-487A-99B6-D81EB84E5A21' }, + ], + }); }); it('should return repositories', async () => { @@ -91,7 +117,11 @@ describe('handleAutocompleteRequest', () => { }, }); - expect(result).toEqual({ results: [{ title: 'repository1' }] }); + expect(result).toEqual({ + results: [ + { title: 'repository1', id: 'F2F0DAF7-B4D6-4694-A131-C2478A200AC5' }, + ], + }); }); it('should return branches', async () => { @@ -104,7 +134,7 @@ describe('handleAutocompleteRequest', () => { }, }); - expect(result).toEqual({ results: [{ title: 'branch1' }] }); + expect(result).toEqual({ results: [{ title: 'branch1', id: 'branch1' }] }); }); it('should throw an error when passing an invalid resource', async () => { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 542e58574e..3439ca616a 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -25,7 +25,7 @@ export async function handleAutocompleteRequest({ resource: string; token: string; context: Record; -}): Promise<{ results: { title: string; id?: string }[] }> { +}): Promise<{ results: { title?: string; id: string }[] }> { const client = BitbucketCloudClient.fromConfig({ host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0', @@ -92,12 +92,15 @@ export async function handleAutocompleteRequest({ 'Missing workspace and/or repository context parameter', ); - const results: { title: string }[] = []; + const results: { title: string; id: string }[] = []; for await (const page of client .listBranchesByRepository(context.repository, context.workspace) .iteratePages()) { - const names = [...page.values!].map(p => ({ title: p.name! })); + const names = [...page.values!].map(p => ({ + title: p.name!, + id: p.name!, + })); results.push(...names); } diff --git a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts new file mode 100644 index 0000000000..4fa906686c --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { ScmIntegrationRegistry } from '@backstage/integration'; +import { createHandleAutocompleteRequest } from './autocomplete'; + +const mockGetClient = require('../util').getClient; + +jest.mock('../util', () => ({ + getClient: jest.fn(), +})); + +describe('handleAutocompleteRequest', () => { + const mockIntegrations = {} as ScmIntegrationRegistry; + const mockClient = { + Groups: { + all: jest.fn(), + allProjects: jest.fn(), + }, + Users: { + showCurrentUser: jest.fn(), + allProjects: jest.fn(), + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockGetClient.mockReturnValue(mockClient); + }); + + it('should return groups and current user', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + mockClient.Groups.all.mockResolvedValueOnce([ + { full_path: 'group1', id: 1 }, + { full_path: 'group2', id: 2 }, + ]); + mockClient.Groups.all.mockResolvedValueOnce([]); + mockClient.Users.showCurrentUser.mockResolvedValue({ + username: 'user1', + id: 3, + }); + + const result = await handleAutocompleteRequest({ + resource: 'groups', + token: 'token', + context: {}, + }); + + expect(result).toEqual({ + results: [ + { title: 'group1', id: '1' }, + { title: 'group2', id: '2' }, + { title: 'user1', id: '3' }, + ], + }); + }); + + it('should return repositories for a group', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + mockClient.Users.showCurrentUser.mockResolvedValue({ id: 3 }); + mockClient.Groups.allProjects.mockResolvedValue([ + { name: 'repo1', id: 1 }, + { name: 'repo2', id: 2 }, + ]); + + const result = await handleAutocompleteRequest({ + resource: 'repositories', + token: 'token', + context: { id: '1' }, + }); + + expect(result).toEqual({ + results: [ + { title: 'repo1', id: '1' }, + { title: 'repo2', id: '2' }, + ], + }); + }); + + it('should return repositories for a user', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + mockClient.Users.showCurrentUser.mockResolvedValue({ id: 1 }); + mockClient.Users.allProjects.mockResolvedValue([ + { name: 'repo1', id: 1 }, + { name: 'repo2', id: 2 }, + ]); + + const result = await handleAutocompleteRequest({ + resource: 'repositories', + token: 'token', + context: { id: '1' }, + }); + + expect(result).toEqual({ + results: [ + { title: 'repo1', id: '1' }, + { title: 'repo2', id: '2' }, + ], + }); + }); + + 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); + }); + + it('should throw an error if context id is missing for repositories', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + await expect( + handleAutocompleteRequest({ + resource: 'repositories', + token: 'token', + context: {}, + }), + ).rejects.toThrow(InputError); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts index 2a15c0c99e..6e9a7f79c4 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/autocomplete/autocomplete.ts @@ -31,8 +31,8 @@ export function createHandleAutocompleteRequest(options: { context: Record; }): Promise<{ results: { - title: string; - id?: string; + title?: string; + id: string; }[]; }> { const { integrations } = options; diff --git a/plugins/scaffolder-node/src/alpha.ts b/plugins/scaffolder-node/src/alpha.ts index a8b7bde76b..38a43d4865 100644 --- a/plugins/scaffolder-node/src/alpha.ts +++ b/plugins/scaffolder-node/src/alpha.ts @@ -95,7 +95,7 @@ export type AutocompleteHandler = ({ resource: string; token: string; context: Record; -}) => Promise<{ results: { title: string; id?: string }[] }>; +}) => Promise<{ results: { title?: string; id: string }[] }>; /** * Extension point for adding autocomplete handler providers diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index 887ccc7cb4..c90c855f9b 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -214,8 +214,8 @@ export interface ScaffolderApi { context?: Record; }): Promise<{ results: { - title: string; - id?: string; + title?: string; + id: string; }[]; }>; cancelTask(taskId: string): Promise; diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index 73dd6c50d2..8e4d989a55 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -245,5 +245,5 @@ export interface ScaffolderApi { provider: string; resource: string; context?: Record; - }): Promise<{ results: { title: string; id?: string }[] }>; + }): Promise<{ results: { title?: string; id: string }[] }>; } diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index 538b2cd9eb..453b482ce2 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -531,7 +531,8 @@ export class ScaffolderClient implements ScaffolderApi_2 { context?: Record; }): Promise<{ results: { - title: string; + title?: string; + id: string; }[]; }>; // (undocumented) diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index df032b536d..01e163bb30 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -22,10 +22,6 @@ import { } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { Observable } from '@backstage/types'; -import qs from 'qs'; -import queryString from 'qs'; -import ObservableImpl from 'zen-observable'; import { ListActionsResponse, LogEvent, @@ -40,10 +36,13 @@ import { ScaffolderTask, TemplateParameterSchema, } from '@backstage/plugin-scaffolder-react'; +import { Observable } from '@backstage/types'; import { EventSourceMessage, fetchEventSource, } from '@microsoft/fetch-event-source'; +import { default as qs, default as queryString } from 'qs'; +import ObservableImpl from 'zen-observable'; /** * An API to interact with the scaffolder backend. @@ -365,7 +364,7 @@ export class ScaffolderClient implements ScaffolderApi { provider: string; resource: string; context?: Record; - }): Promise<{ results: { title: string }[] }> { + }): Promise<{ results: { title?: string; id: string }[] }> { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const url = `${baseUrl}/v2/autocomplete/${provider}/${resource}`; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx index 26853d6d05..aa3b0e0c20 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -14,15 +14,15 @@ * limitations under the License. */ +import { useApi } from '@backstage/core-plugin-api'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import FormControl from '@material-ui/core/FormControl'; -import React, { useCallback, useState } from 'react'; +import FormHelperText from '@material-ui/core/FormHelperText'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; +import React, { useCallback, useState } from 'react'; 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` @@ -66,7 +66,7 @@ export const BitbucketRepoBranchPicker = ({ provider: 'bitbucket-cloud', }) .then(({ results }) => { - setAvailableBranches(results.map(r => r.title)); + setAvailableBranches(results.map(r => r.title!)); }) .catch(() => { setAvailableBranches([]); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index 8ac7be0e97..d190c7b10c 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -63,8 +63,8 @@ export const GitlabRepoPicker = ( setAvailableGroups( results.map(r => { return { - title: r.title, - id: r.id!, + title: r.title!, + id: r.id, }; }), ); @@ -96,7 +96,7 @@ export const GitlabRepoPicker = ( provider: 'gitlab', }) .then(({ results }) => { - onChange({ availableRepos: results.map(r => r.title) }); + onChange({ availableRepos: results.map(r => r.title!) }); }) .catch(() => { onChange({ availableRepos: [] }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts index 9489901d61..414cb1b0ab 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/types.ts @@ -20,6 +20,7 @@ export interface RepoUrlPickerState { organization?: string; workspace?: string; project?: string; + id?: string; availableRepos?: string[]; } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts index c90b357cf1..ce3c938e31 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.test.ts @@ -53,9 +53,10 @@ describe('utils', () => { organization: 'organization', workspace: 'workspace', project: 'backstage', + id: '1234', }), ).toBe( - 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage', + 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage&id=1234', ); }); }); @@ -64,7 +65,7 @@ describe('utils', () => { it('should parse a complete string', () => { expect( parseRepoPickerUrl( - 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage', + 'github.com?owner=owner&repo=backstage&organization=organization&workspace=workspace&project=backstage&id=1234', ), ).toEqual({ host: 'github.com', @@ -73,6 +74,7 @@ describe('utils', () => { organization: 'organization', workspace: 'workspace', project: 'backstage', + id: '1234', }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts index 408b87413b..145a3e256f 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts @@ -37,6 +37,9 @@ export function serializeRepoPickerUrl(data: RepoUrlPickerState) { if (data.project) { params.set('project', data.project); } + if (data.id) { + params.set('id', data.id); + } return `${data.host}?${params.toString()}`; } @@ -50,6 +53,7 @@ export function parseRepoPickerUrl( let organization = ''; let workspace = ''; let project = ''; + let id = ''; try { if (url) { @@ -60,9 +64,10 @@ export function parseRepoPickerUrl( organization = parsed.searchParams.get('organization') || ''; workspace = parsed.searchParams.get('workspace') || ''; project = parsed.searchParams.get('project') || ''; + id = parsed.searchParams.get('id') || ''; } } catch { /* ok */ } - return { host, owner, repoName, organization, workspace, project }; + return { host, owner, repoName, organization, workspace, project, id }; } From 1cce261913a6381591f6caa54a07b62a756b480a Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Wed, 6 Nov 2024 16:18:57 +0100 Subject: [PATCH 5/8] Fix `yarn tsc` Signed-off-by: Severin Wischmann --- .../components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index c2c6b7f0d6..c9fccb40be 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -89,7 +89,7 @@ export const BitbucketRepoPicker = ( provider: 'bitbucket-cloud', }) .then(({ results }) => { - setAvailableWorkspaces(results.map(r => r.title)); + setAvailableWorkspaces(results.map(r => r.title!)); }) .catch(() => { setAvailableWorkspaces([]); @@ -118,7 +118,7 @@ export const BitbucketRepoPicker = ( provider: 'bitbucket-cloud', }) .then(({ results }) => { - setAvailableProjects(results.map(r => r.title)); + setAvailableProjects(results.map(r => r.title!)); }) .catch(() => { setAvailableProjects([]); @@ -148,7 +148,7 @@ export const BitbucketRepoPicker = ( provider: 'bitbucket-cloud', }) .then(({ results }) => { - onChange({ availableRepos: results.map(r => r.title) }); + onChange({ availableRepos: results.map(r => r.title!) }); }) .catch(() => { onChange({ availableRepos: [] }); From 6011c8d88a92868b3004e1cc900bfc52b6990def Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Wed, 6 Nov 2024 16:39:27 +0100 Subject: [PATCH 6/8] Update api-report Signed-off-by: Severin Wischmann --- plugins/scaffolder-backend-module-gitlab/report.api.md | 6 +++--- plugins/scaffolder-backend/report.api.md | 2 +- plugins/scaffolder-node/report-alpha.api.md | 4 ++-- plugins/scaffolder/report-alpha.api.md | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/report.api.md b/plugins/scaffolder-backend-module-gitlab/report.api.md index 0cf1c41717..a6e1d96d2c 100644 --- a/plugins/scaffolder-backend-module-gitlab/report.api.md +++ b/plugins/scaffolder-backend-module-gitlab/report.api.md @@ -33,8 +33,8 @@ export const createGitlabIssueAction: (options: { projectId: number; labels?: string | undefined; description?: string | undefined; - weight?: number | undefined; token?: string | undefined; + weight?: number | undefined; assignees?: number[] | undefined; createdAt?: string | undefined; confidential?: boolean | undefined; @@ -198,7 +198,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; + commitAction?: 'auto' | 'update' | 'skip' | 'delete' | 'create' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; @@ -234,8 +234,8 @@ export const editGitlabIssueAction: (options: { title?: string | undefined; labels?: string | undefined; description?: string | undefined; - weight?: number | undefined; token?: string | undefined; + weight?: number | undefined; assignees?: number[] | undefined; addLabels?: string | undefined; confidential?: boolean | undefined; diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index cb5da51ea4..e631020f4f 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -354,7 +354,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; + commitAction?: 'auto' | 'update' | 'skip' | 'delete' | 'create' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/plugins/scaffolder-node/report-alpha.api.md b/plugins/scaffolder-node/report-alpha.api.md index 4ecbc71358..eef9acc1c1 100644 --- a/plugins/scaffolder-node/report-alpha.api.md +++ b/plugins/scaffolder-node/report-alpha.api.md @@ -22,8 +22,8 @@ export type AutocompleteHandler = ({ context: Record; }) => Promise<{ results: { - title: string; - id?: string; + title?: string; + id: string; }[]; }>; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 4253b2b914..8896d3dfda 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -262,15 +262,15 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'ongoingTask.title': 'Run of'; readonly 'ongoingTask.contextMenu.cancel': 'Cancel'; readonly 'ongoingTask.contextMenu.startOver': 'Start Over'; - readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.hideLogs': 'Hide Logs'; readonly 'ongoingTask.contextMenu.showLogs': 'Show Logs'; readonly 'ongoingTask.contextMenu.hideButtonBar': 'Hide Button Bar'; + readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.showButtonBar': 'Show Button Bar'; readonly 'ongoingTask.subtitle': 'Task {{taskId}}'; + readonly 'ongoingTask.cancelButtonTitle': 'Cancel'; readonly 'ongoingTask.pageTitle.hasTemplateName': 'Run of {{templateName}}'; readonly 'ongoingTask.pageTitle.noTemplateName': 'Scaffolder Run'; - readonly 'ongoingTask.cancelButtonTitle': 'Cancel'; readonly 'ongoingTask.retryButtonTitle': 'Retry'; readonly 'ongoingTask.startOverButtonTitle': 'Start Over'; readonly 'ongoingTask.hideLogsButtonTitle': 'Hide Logs'; @@ -335,8 +335,8 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateEditorToolbar.addToCatalogDialogTitle': 'Publish changes'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsIntroduction': 'Follow the instructions below to create or update a template:'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog'; - readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; readonly 'templateEditorToolbarFileMenu.button': 'File'; readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory'; readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory'; From 452a2302c3a1f05376b4bc7f1fd734fed879c6b7 Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Tue, 12 Nov 2024 17:20:09 +0100 Subject: [PATCH 7/8] Update api reports Signed-off-by: Severin Wischmann --- plugins/scaffolder-backend-module-gitlab/report.api.md | 6 +++--- plugins/scaffolder-backend/report.api.md | 2 +- plugins/scaffolder/report-alpha.api.md | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder-backend-module-gitlab/report.api.md b/plugins/scaffolder-backend-module-gitlab/report.api.md index a6e1d96d2c..0cf1c41717 100644 --- a/plugins/scaffolder-backend-module-gitlab/report.api.md +++ b/plugins/scaffolder-backend-module-gitlab/report.api.md @@ -33,8 +33,8 @@ export const createGitlabIssueAction: (options: { projectId: number; labels?: string | undefined; description?: string | undefined; - token?: string | undefined; weight?: number | undefined; + token?: string | undefined; assignees?: number[] | undefined; createdAt?: string | undefined; confidential?: boolean | undefined; @@ -198,7 +198,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'skip' | 'delete' | 'create' | undefined; + commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; @@ -234,8 +234,8 @@ export const editGitlabIssueAction: (options: { title?: string | undefined; labels?: string | undefined; description?: string | undefined; - token?: string | undefined; weight?: number | undefined; + token?: string | undefined; assignees?: number[] | undefined; addLabels?: string | undefined; confidential?: boolean | undefined; diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index e631020f4f..cb5da51ea4 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -354,7 +354,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'auto' | 'update' | 'skip' | 'delete' | 'create' | undefined; + commitAction?: 'auto' | 'update' | 'delete' | 'create' | 'skip' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 8896d3dfda..4253b2b914 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -262,15 +262,15 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'ongoingTask.title': 'Run of'; readonly 'ongoingTask.contextMenu.cancel': 'Cancel'; readonly 'ongoingTask.contextMenu.startOver': 'Start Over'; + readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.hideLogs': 'Hide Logs'; readonly 'ongoingTask.contextMenu.showLogs': 'Show Logs'; readonly 'ongoingTask.contextMenu.hideButtonBar': 'Hide Button Bar'; - readonly 'ongoingTask.contextMenu.retry': 'Retry'; readonly 'ongoingTask.contextMenu.showButtonBar': 'Show Button Bar'; readonly 'ongoingTask.subtitle': 'Task {{taskId}}'; - readonly 'ongoingTask.cancelButtonTitle': 'Cancel'; readonly 'ongoingTask.pageTitle.hasTemplateName': 'Run of {{templateName}}'; readonly 'ongoingTask.pageTitle.noTemplateName': 'Scaffolder Run'; + readonly 'ongoingTask.cancelButtonTitle': 'Cancel'; readonly 'ongoingTask.retryButtonTitle': 'Retry'; readonly 'ongoingTask.startOverButtonTitle': 'Start Over'; readonly 'ongoingTask.hideLogsButtonTitle': 'Hide Logs'; @@ -335,8 +335,8 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateEditorToolbar.addToCatalogDialogTitle': 'Publish changes'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsIntroduction': 'Follow the instructions below to create or update a template:'; readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog'; - readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; + readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; readonly 'templateEditorToolbarFileMenu.button': 'File'; readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory'; readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory'; From 5e74f87e0ce4b636435dac8bd67e983de23531fd Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Wed, 13 Nov 2024 13:51:30 +0100 Subject: [PATCH 8/8] Use id as title was before Signed-off-by: Severin Wischmann --- .../src/autocomplete/autocomplete.test.ts | 20 ++++------------ .../src/autocomplete/autocomplete.ts | 18 ++++++-------- .../BitbucketRepoPicker.test.tsx | 14 +++++------ .../RepoUrlPicker/BitbucketRepoPicker.tsx | 10 +++++--- .../fields/RepoUrlPicker/GitlabRepoPicker.tsx | 6 ++++- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 6 ++++- .../RepoUrlPickerRepoName.test.tsx | 16 ++++++------- .../RepoUrlPicker/RepoUrlPickerRepoName.tsx | 24 ++++++++++++------- .../components/fields/RepoUrlPicker/types.ts | 7 +++++- 9 files changed, 65 insertions(+), 56 deletions(-) diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts index 1729593d2b..10b277963b 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts @@ -26,7 +26,6 @@ describe('handleAutocompleteRequest', () => { values: [ { slug: 'workspace1', - uuid: '486F4F29-9B88-4BE8-9092-24BEB8A5F3B3', }, ], }, @@ -35,9 +34,7 @@ describe('handleAutocompleteRequest', () => { listProjectsByWorkspace: jest.fn().mockReturnValue({ iteratePages: jest.fn().mockReturnValue([ { - values: [ - { key: 'project1', uuid: '70F065E3-CE7C-487A-99B6-D81EB84E5A21' }, - ], + values: [{ key: 'project1' }], }, ]), }), @@ -47,7 +44,6 @@ describe('handleAutocompleteRequest', () => { values: [ { slug: 'repository1', - uuid: 'F2F0DAF7-B4D6-4694-A131-C2478A200AC5', }, ], }, @@ -85,9 +81,7 @@ describe('handleAutocompleteRequest', () => { }); expect(result).toEqual({ - results: [ - { title: 'workspace1', id: '486F4F29-9B88-4BE8-9092-24BEB8A5F3B3' }, - ], + results: [{ id: 'workspace1' }], }); }); @@ -101,9 +95,7 @@ describe('handleAutocompleteRequest', () => { }); expect(result).toEqual({ - results: [ - { title: 'project1', id: '70F065E3-CE7C-487A-99B6-D81EB84E5A21' }, - ], + results: [{ id: 'project1' }], }); }); @@ -118,9 +110,7 @@ describe('handleAutocompleteRequest', () => { }); expect(result).toEqual({ - results: [ - { title: 'repository1', id: 'F2F0DAF7-B4D6-4694-A131-C2478A200AC5' }, - ], + results: [{ id: 'repository1' }], }); }); @@ -134,7 +124,7 @@ describe('handleAutocompleteRequest', () => { }, }); - expect(result).toEqual({ results: [{ title: 'branch1', id: 'branch1' }] }); + expect(result).toEqual({ results: [{ id: 'branch1' }] }); }); it('should throw an error when passing an invalid resource', async () => { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 3439ca616a..3d1cdd5748 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -34,12 +34,11 @@ export async function handleAutocompleteRequest({ switch (resource) { case 'workspaces': { - const results: { title: string; id: string }[] = []; + const results: { title?: string; id: string }[] = []; for await (const page of client.listWorkspaces().iteratePages()) { const slugs = [...page.values!].map(p => ({ - title: p.slug!, - id: p.uuid!, + id: p.slug!, })); results.push(...slugs); } @@ -50,14 +49,13 @@ export async function handleAutocompleteRequest({ if (!context.workspace) throw new InputError('Missing workspace context parameter'); - const results: { title: string; id: string }[] = []; + const results: { title?: string; id: string }[] = []; for await (const page of client .listProjectsByWorkspace(context.workspace) .iteratePages()) { const keys = [...page.values!].map(p => ({ - title: p.key!, - id: p.uuid!, + id: p.key!, })); results.push(...keys); } @@ -70,7 +68,7 @@ export async function handleAutocompleteRequest({ 'Missing workspace and/or project context parameter', ); - const results: { title: string; id: string }[] = []; + const results: { title?: string; id: string }[] = []; for await (const page of client .listRepositoriesByWorkspace(context.workspace, { @@ -78,8 +76,7 @@ export async function handleAutocompleteRequest({ }) .iteratePages()) { const slugs = [...page.values!].map(p => ({ - title: p.slug!, - id: p.uuid!, + id: p.slug!, })); results.push(...slugs); } @@ -92,13 +89,12 @@ export async function handleAutocompleteRequest({ 'Missing workspace and/or repository context parameter', ); - const results: { title: string; id: string }[] = []; + const results: { title?: string; id: string }[] = []; for await (const page of client .listBranchesByRepository(context.repository, context.workspace) .iteratePages()) { const names = [...page.values!].map(p => ({ - title: p.name!, id: p.name!, })); results.push(...names); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx index 05d04886e4..af3952ff51 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.test.tsx @@ -14,22 +14,22 @@ * limitations under the License. */ -import React from 'react'; -import { BitbucketRepoPicker } from './BitbucketRepoPicker'; -import { fireEvent, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { ScaffolderApi, scaffolderApiRef, } from '@backstage/plugin-scaffolder-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { fireEvent, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; import { act } from 'react-dom/test-utils'; +import { BitbucketRepoPicker } from './BitbucketRepoPicker'; describe('BitbucketRepoPicker', () => { const scaffolderApiMock: Partial = { autocomplete: jest.fn().mockImplementation(opts => Promise.resolve({ - results: [{ title: `${opts.resource}_example` }], + results: [{ id: `${opts.resource}_example` }], }), ), }; @@ -266,7 +266,7 @@ describe('BitbucketRepoPicker', () => { // Verify that the available repos are updated await waitFor(() => expect(onChange).toHaveBeenCalledWith({ - availableRepos: ['repositories_example'], + availableRepos: [{ name: 'repositories_example' }], }), ); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx index c9fccb40be..45a6439a63 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/BitbucketRepoPicker.tsx @@ -89,7 +89,7 @@ export const BitbucketRepoPicker = ( provider: 'bitbucket-cloud', }) .then(({ results }) => { - setAvailableWorkspaces(results.map(r => r.title!)); + setAvailableWorkspaces(results.map(r => r.id)); }) .catch(() => { setAvailableWorkspaces([]); @@ -118,7 +118,7 @@ export const BitbucketRepoPicker = ( provider: 'bitbucket-cloud', }) .then(({ results }) => { - setAvailableProjects(results.map(r => r.title!)); + setAvailableProjects(results.map(r => r.id)); }) .catch(() => { setAvailableProjects([]); @@ -148,7 +148,11 @@ export const BitbucketRepoPicker = ( provider: 'bitbucket-cloud', }) .then(({ results }) => { - onChange({ availableRepos: results.map(r => r.title!) }); + onChange({ + availableRepos: results.map(r => { + return { name: r.id }; + }), + }); }) .catch(() => { onChange({ availableRepos: [] }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx index d190c7b10c..d97c9431ee 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GitlabRepoPicker.tsx @@ -96,7 +96,11 @@ export const GitlabRepoPicker = ( provider: 'gitlab', }) .then(({ results }) => { - onChange({ availableRepos: results.map(r => r.title!) }); + onChange({ + availableRepos: results.map(r => { + return { name: r.title!, id: r.id }; + }), + }); }) .catch(() => { onChange({ availableRepos: [] }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index c0e52e1826..18da234012 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -242,7 +242,11 @@ export const RepoUrlPicker = ( repoName={state.repoName} allowedRepos={allowedRepos} onChange={repo => - setState(prevState => ({ ...prevState, repoName: repo })) + setState(prevState => ({ + ...prevState, + repoName: repo.name, + id: repo.id || '', + })) } rawErrors={rawErrors} availableRepos={state.availableRepos} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.test.tsx index ec4853c714..f78abc2dfb 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.test.tsx @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; +import { renderInTestApp } from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import React from 'react'; import { act } from 'react-dom/test-utils'; -import { renderInTestApp } from '@backstage/test-utils'; +import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; describe('RepoUrlPickerRepoName', () => { it('should call onChange with the first allowed repo if there is none set already', async () => { @@ -32,7 +32,7 @@ describe('RepoUrlPickerRepoName', () => { />, ); - expect(onChange).toHaveBeenCalledWith('foo'); + expect(onChange).toHaveBeenCalledWith({ name: 'foo' }); }); it('should render a dropdown of all the options', async () => { @@ -78,11 +78,11 @@ describe('RepoUrlPickerRepoName', () => { textArea.blur(); }); - expect(onChange).toHaveBeenCalledWith('foo'); + expect(onChange).toHaveBeenCalledWith({ name: 'foo' }); }); it('should autocomplete with provided availableRepos', async () => { - const availableRepos = ['foo', 'bar']; + const availableRepos = [{ name: 'foo' }, { name: 'bar' }]; const onChange = jest.fn(); @@ -100,11 +100,11 @@ describe('RepoUrlPickerRepoName', () => { // Verify that available repos are shown for (const repo of availableRepos) { - expect(getByText(repo)).toBeInTheDocument(); + expect(getByText(repo.name)).toBeInTheDocument(); } // Verify that selecting an option calls onChange - await userEvent.click(getByText(availableRepos[0])); + await userEvent.click(getByText(availableRepos[0].name)); expect(onChange).toHaveBeenCalledWith(availableRepos[0]); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx index b1152d3c1f..d23a4bcd86 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerRepoName.tsx @@ -13,21 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useEffect } from 'react'; import { Select, SelectItem } from '@backstage/core-components'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; -import Autocomplete from '@material-ui/lab/Autocomplete'; import TextField from '@material-ui/core/TextField'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import React, { useEffect } from 'react'; import { scaffolderTranslationRef } from '../../../translation'; +import { AvailableRepositories } from './types'; export const RepoUrlPickerRepoName = (props: { repoName?: string; allowedRepos?: string[]; - onChange: (host: string) => void; + onChange: (chosenRepo: AvailableRepositories) => void; rawErrors: string[]; - availableRepos?: string[]; + availableRepos?: AvailableRepositories[]; }) => { const { repoName, allowedRepos, onChange, rawErrors, availableRepos } = props; const { t } = useTranslationRef(scaffolderTranslationRef); @@ -37,7 +38,7 @@ export const RepoUrlPickerRepoName = (props: { if (!repoName) { // Set the first of the allowedRepos option if that available if (allowedRepos?.length) { - onChange(allowedRepos[0]); + onChange({ name: allowedRepos[0] }); } } }, [allowedRepos, repoName, onChange]); @@ -58,7 +59,9 @@ export const RepoUrlPickerRepoName = (props: { native label={t('fields.repoUrlPicker.repository.title')} onChange={selected => - onChange(String(Array.isArray(selected) ? selected[0] : selected)) + onChange({ + name: String(Array.isArray(selected) ? selected[0] : selected), + }) } disabled={allowedRepos.length === 1} selected={repoName} @@ -68,9 +71,12 @@ export const RepoUrlPickerRepoName = (props: { { - onChange(newValue || ''); + const selectedRepo = availableRepos?.find( + r => r.name === newValue, + ); + onChange(selectedRepo || { name: newValue || '' }); }} - options={availableRepos || []} + options={(availableRepos || []).map(r => r.name)} renderInput={params => ( = T & {