From 38682faf86d39759a4b8262129ac57be390b169f Mon Sep 17 00:00:00 2001 From: Severin Wischmann Date: Wed, 6 Nov 2024 16:14:06 +0100 Subject: [PATCH] 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 }; }