Merge pull request #27367 from swnia/repopicker-gitlab-autocomplete
Add autocomplete to GitlabRepoUrlPicker
This commit is contained in:
@@ -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
|
||||
+34
-14
@@ -14,26 +14,40 @@
|
||||
* 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
}),
|
||||
listProjectsByWorkspace: jest.fn().mockReturnValue({
|
||||
iteratePages: jest
|
||||
.fn()
|
||||
.mockReturnValue([{ values: [{ key: 'project1' }] }]),
|
||||
iteratePages: jest.fn().mockReturnValue([
|
||||
{
|
||||
values: [{ key: 'project1' }],
|
||||
},
|
||||
]),
|
||||
}),
|
||||
listRepositoriesByWorkspace: jest.fn().mockReturnValue({
|
||||
iteratePages: jest
|
||||
.fn()
|
||||
.mockReturnValue([{ values: [{ slug: 'repository1' }] }]),
|
||||
iteratePages: jest.fn().mockReturnValue([
|
||||
{
|
||||
values: [
|
||||
{
|
||||
slug: 'repository1',
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
}),
|
||||
listBranchesByRepository: jest.fn().mockReturnValue({
|
||||
iteratePages: jest
|
||||
@@ -66,7 +80,9 @@ describe('handleAutocompleteRequest', () => {
|
||||
resource: 'workspaces',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ results: [{ title: 'workspace1' }] });
|
||||
expect(result).toEqual({
|
||||
results: [{ id: 'workspace1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return projects', async () => {
|
||||
@@ -78,7 +94,9 @@ describe('handleAutocompleteRequest', () => {
|
||||
resource: 'projects',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ results: [{ title: 'project1' }] });
|
||||
expect(result).toEqual({
|
||||
results: [{ id: 'project1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return repositories', async () => {
|
||||
@@ -91,7 +109,9 @@ describe('handleAutocompleteRequest', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({ results: [{ title: 'repository1' }] });
|
||||
expect(result).toEqual({
|
||||
results: [{ id: 'repository1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return branches', async () => {
|
||||
@@ -104,7 +124,7 @@ describe('handleAutocompleteRequest', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({ results: [{ title: 'branch1' }] });
|
||||
expect(result).toEqual({ results: [{ 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 }[] }> {
|
||||
}): Promise<{ results: { title?: string; id: string }[] }> {
|
||||
const client = BitbucketCloudClient.fromConfig({
|
||||
host: 'bitbucket.org',
|
||||
apiBaseUrl: 'https://api.bitbucket.org/2.0',
|
||||
@@ -34,10 +34,12 @@ 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 => ({
|
||||
id: p.slug!,
|
||||
}));
|
||||
results.push(...slugs);
|
||||
}
|
||||
|
||||
@@ -47,12 +49,14 @@ 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 => ({
|
||||
id: p.key!,
|
||||
}));
|
||||
results.push(...keys);
|
||||
}
|
||||
|
||||
@@ -64,14 +68,16 @@ 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 => ({
|
||||
id: p.slug!,
|
||||
}));
|
||||
results.push(...slugs);
|
||||
}
|
||||
|
||||
@@ -83,12 +89,14 @@ 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 => ({
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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<string, string>;
|
||||
}): 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}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 }),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -22,7 +22,8 @@ export type AutocompleteHandler = ({
|
||||
context: Record<string, string>;
|
||||
}) => Promise<{
|
||||
results: {
|
||||
title: string;
|
||||
title?: string;
|
||||
id: string;
|
||||
}[];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -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<string, string>;
|
||||
}) => Promise<{ results: { title: string }[] }>;
|
||||
}) => Promise<{ results: { title?: string; id: string }[] }>;
|
||||
|
||||
/**
|
||||
* Extension point for adding autocomplete handler providers
|
||||
|
||||
@@ -214,7 +214,8 @@ export interface ScaffolderApi {
|
||||
context?: Record<string, string>;
|
||||
}): Promise<{
|
||||
results: {
|
||||
title: 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 }[] }>;
|
||||
}): 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([]);
|
||||
|
||||
@@ -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<ScaffolderApi> = {
|
||||
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' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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`
|
||||
@@ -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: [] });
|
||||
|
||||
@@ -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<ScaffolderApi> = {
|
||||
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(
|
||||
<GitlabRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GitlabRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>
|
||||
,
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(await findByText('owner1')).toBeInTheDocument();
|
||||
@@ -40,12 +54,15 @@ describe('GitlabRepoPicker', () => {
|
||||
const onChange = jest.fn();
|
||||
const allowedOwners = ['owner1', 'owner2'];
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>
|
||||
,
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await fireEvent.change(getByRole('combobox'), {
|
||||
@@ -59,12 +76,15 @@ describe('GitlabRepoPicker', () => {
|
||||
const onChange = jest.fn();
|
||||
const allowedOwners = ['owner1'];
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>
|
||||
,
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GitlabRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
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' });
|
||||
});
|
||||
|
||||
@@ -13,28 +13,101 @@
|
||||
* 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 => {
|
||||
return { name: r.title!, id: r.id };
|
||||
}),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
onChange({ availableRepos: [] });
|
||||
});
|
||||
}, [scaffolderApi, accessToken, host, owner, onChange, availableGroups]);
|
||||
|
||||
useDebounce(updateAvailableRepositories, 500, [updateAvailableRepositories]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,12 +137,21 @@ export const GitlabRepoPicker = (
|
||||
</FormHelperText>
|
||||
</>
|
||||
) : (
|
||||
<TextField
|
||||
id="ownerInput"
|
||||
label={t('fields.gitlabRepoPicker.owner.inputTitle')}
|
||||
onChange={e => onChange({ owner: e.target.value })}
|
||||
helperText={t('fields.gitlabRepoPicker.owner.description')}
|
||||
<Autocomplete
|
||||
value={owner}
|
||||
onChange={(_, newValue) => {
|
||||
onChange({ owner: newValue || '' });
|
||||
}}
|
||||
options={availableGroups.map(group => group.title)}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t('fields.gitlabRepoPicker.owner.title')}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
freeSolo
|
||||
autoSelect
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
@@ -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' && (
|
||||
@@ -238,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}
|
||||
|
||||
+8
-8
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
<Autocomplete
|
||||
value={repoName}
|
||||
onInputChange={(_, newValue) => {
|
||||
onChange(newValue || '');
|
||||
const selectedRepo = availableRepos?.find(
|
||||
r => r.name === newValue,
|
||||
);
|
||||
onChange(selectedRepo || { name: newValue || '' });
|
||||
}}
|
||||
options={availableRepos || []}
|
||||
options={(availableRepos || []).map(r => r.name)}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface AvailableRepositories {
|
||||
name: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface RepoUrlPickerState {
|
||||
host?: string;
|
||||
owner?: string;
|
||||
@@ -20,7 +25,8 @@ export interface RepoUrlPickerState {
|
||||
organization?: string;
|
||||
workspace?: string;
|
||||
project?: string;
|
||||
availableRepos?: string[];
|
||||
id?: string;
|
||||
availableRepos?: AvailableRepositories[];
|
||||
}
|
||||
|
||||
export type BaseRepoUrlPickerProps<T extends {} = {}> = T & {
|
||||
|
||||
@@ -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