Make id mandatory and title optional
Signed-off-by: Severin Wischmann <severinwischmann@nianticlabs.com>
This commit is contained in:
+44
-14
@@ -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<BitbucketCloudClient> = {
|
||||
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 () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function handleAutocompleteRequest({
|
||||
resource: string;
|
||||
token: string;
|
||||
context: Record<string, string>;
|
||||
}): 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -31,8 +31,8 @@ export function createHandleAutocompleteRequest(options: {
|
||||
context: Record<string, string>;
|
||||
}): Promise<{
|
||||
results: {
|
||||
title: string;
|
||||
id?: string;
|
||||
title?: string;
|
||||
id: string;
|
||||
}[];
|
||||
}> {
|
||||
const { integrations } = options;
|
||||
|
||||
@@ -95,7 +95,7 @@ export type AutocompleteHandler = ({
|
||||
resource: string;
|
||||
token: string;
|
||||
context: Record<string, string>;
|
||||
}) => Promise<{ results: { title: string; id?: string }[] }>;
|
||||
}) => Promise<{ results: { title?: string; id: string }[] }>;
|
||||
|
||||
/**
|
||||
* Extension point for adding autocomplete handler providers
|
||||
|
||||
@@ -214,8 +214,8 @@ export interface ScaffolderApi {
|
||||
context?: Record<string, string>;
|
||||
}): Promise<{
|
||||
results: {
|
||||
title: string;
|
||||
id?: string;
|
||||
title?: string;
|
||||
id: string;
|
||||
}[];
|
||||
}>;
|
||||
cancelTask(taskId: string): Promise<void>;
|
||||
|
||||
@@ -245,5 +245,5 @@ export interface ScaffolderApi {
|
||||
provider: string;
|
||||
resource: string;
|
||||
context?: Record<string, string>;
|
||||
}): Promise<{ results: { title: string; id?: string }[] }>;
|
||||
}): Promise<{ results: { title?: string; id: string }[] }>;
|
||||
}
|
||||
|
||||
@@ -531,7 +531,8 @@ export class ScaffolderClient implements ScaffolderApi_2 {
|
||||
context?: Record<string, string>;
|
||||
}): Promise<{
|
||||
results: {
|
||||
title: string;
|
||||
title?: string;
|
||||
id: string;
|
||||
}[];
|
||||
}>;
|
||||
// (undocumented)
|
||||
|
||||
@@ -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<string, string>;
|
||||
}): Promise<{ results: { title: string }[] }> {
|
||||
}): Promise<{ results: { title?: string; id: string }[] }> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
|
||||
const url = `${baseUrl}/v2/autocomplete/${provider}/${resource}`;
|
||||
|
||||
+4
-4
@@ -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([]);
|
||||
|
||||
@@ -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: [] });
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface RepoUrlPickerState {
|
||||
organization?: string;
|
||||
workspace?: string;
|
||||
project?: string;
|
||||
id?: string;
|
||||
availableRepos?: string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user