Use default RepoUrlPicker for bitbucket fields
Also updated the default bitbucket publisher to handle workspace and project fields Signed-off-by: Mustansar Anwar ul Samad <mustansar.samad@gmail.com>
This commit is contained in:
Vendored
-38
@@ -58,44 +58,6 @@ export interface Config {
|
||||
* @visibility secret
|
||||
*/
|
||||
appPassword?: string;
|
||||
/**
|
||||
* Bitbucket workspaces
|
||||
* @visibility frontend
|
||||
*/
|
||||
workspaces?: Array<{
|
||||
/**
|
||||
* The name of workspace
|
||||
* @visibility frontend
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The description for a workspace
|
||||
* @visibility frontend
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Bitbucket projects in the the workspace
|
||||
* @visibility frontend
|
||||
*/
|
||||
projects?: Array<{
|
||||
/**
|
||||
* The name of the project in bitbucket
|
||||
* @visibility frontend
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The key for the project in bitbucket
|
||||
* @visibility frontend
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* The description for the project in bitbucket
|
||||
* @visibility frontend
|
||||
*/
|
||||
description?: string;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
}>;
|
||||
|
||||
/** Integration configuration for GitHub */
|
||||
|
||||
@@ -60,29 +60,8 @@ export type BitbucketIntegrationConfig = {
|
||||
* See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/
|
||||
*/
|
||||
appPassword?: string;
|
||||
|
||||
/**
|
||||
* Bitbucket workspaces, can be organization or user
|
||||
*/
|
||||
workspaces?: BitbucketWorkspace[];
|
||||
|
||||
};
|
||||
|
||||
export type BitbucketWorkspace = {
|
||||
name: string;
|
||||
description?: string;
|
||||
/**
|
||||
* Bitbucket projects in the organization to add repos to
|
||||
*/
|
||||
projects?: BitbucketProject[];
|
||||
}
|
||||
|
||||
export type BitbucketProject = {
|
||||
name: string;
|
||||
key: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a single Bitbucket integration config.
|
||||
*
|
||||
@@ -96,15 +75,6 @@ export function readBitbucketIntegrationConfig(
|
||||
const token = config.getOptionalString('token');
|
||||
const username = config.getOptionalString('username');
|
||||
const appPassword = config.getOptionalString('appPassword');
|
||||
const workspaces = config.getOptionalConfigArray('workspaces')?.map(w => ({
|
||||
name: w.getString('name'),
|
||||
description: w.getOptionalString('description'),
|
||||
projects : w.getOptionalConfigArray('projects')?.map(p => ({
|
||||
name: p.getString('name'),
|
||||
key: p.getString('key'),
|
||||
description: p.getOptionalString('description'),
|
||||
})),
|
||||
}));
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
@@ -124,7 +94,6 @@ export function readBitbucketIntegrationConfig(
|
||||
token,
|
||||
username,
|
||||
appPassword,
|
||||
workspaces
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+10
-5
@@ -1,9 +1,9 @@
|
||||
apiVersion: backstage.io/v1beta2
|
||||
kind: Template
|
||||
metadata:
|
||||
name: bitbucket-cloud
|
||||
title: Test Bitbucket Cloud RepoUrlPicker template
|
||||
description: scaffolder v1beta2 template demo publishing to bitbucket cloud
|
||||
name: bitbucket-demo
|
||||
title: Test Bitbucket RepoUrlPicker template
|
||||
description: scaffolder v1beta2 template demo publishing to bitbucket
|
||||
spec:
|
||||
owner: backstage/techdocs-core
|
||||
type: service
|
||||
@@ -16,7 +16,12 @@ spec:
|
||||
repoUrl:
|
||||
title: Repository Location
|
||||
type: string
|
||||
ui:field: RepoUrlPickerBitbucketCloud
|
||||
ui:field: RepoUrlPicker
|
||||
ui:options:
|
||||
allowedHosts:
|
||||
- github.com
|
||||
- bitbucket.org
|
||||
- server.bitbucket.com
|
||||
- title: Fill in some steps
|
||||
required:
|
||||
- name
|
||||
@@ -57,7 +62,7 @@ spec:
|
||||
|
||||
- id: publish
|
||||
name: Publish
|
||||
action: publish:bitbucketcloud
|
||||
action: publish:bitbucket
|
||||
input:
|
||||
allowedHosts: ['bitbucket.org']
|
||||
description: 'This is {{ parameters.name }}'
|
||||
@@ -11,4 +11,4 @@ spec:
|
||||
- ./springboot-grpc-template/template.yaml
|
||||
- ./v1beta2-demo/template.yaml
|
||||
- ./pull-request/template.yaml
|
||||
- ./bitbucket-cloud-demo/template.yaml
|
||||
- ./bitbucket-demo/template.yaml
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
import {
|
||||
createPublishAzureAction,
|
||||
createPublishBitbucketAction,
|
||||
createPublishBitbucketCloudAction,
|
||||
createPublishGithubAction,
|
||||
createPublishGithubPullRequestAction,
|
||||
createPublishGitlabAction,
|
||||
@@ -79,9 +78,6 @@ export const createBuiltinActions = (options: {
|
||||
integrations,
|
||||
config,
|
||||
}),
|
||||
createPublishBitbucketCloudAction({
|
||||
integrations,
|
||||
}),
|
||||
createPublishAzureAction({
|
||||
integrations,
|
||||
config,
|
||||
|
||||
+27
-20
@@ -50,7 +50,7 @@ describe('publish:bitbucket', () => {
|
||||
const action = createPublishBitbucketAction({ integrations, config });
|
||||
const mockContext = {
|
||||
input: {
|
||||
repoUrl: 'bitbucket.org?repo=repo&owner=owner',
|
||||
repoUrl: 'bitbucket.org?type=bitbucket&workspace=workspace&project=project&repo=repo',
|
||||
repoVisibility: 'private',
|
||||
},
|
||||
workspacePath: 'lol',
|
||||
@@ -70,14 +70,21 @@ describe('publish:bitbucket', () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.com?repo=bob' },
|
||||
input: { repoUrl: 'bitbucket.org?type=bitbucket&project=project&repo=repo' },
|
||||
}),
|
||||
).rejects.toThrow(/missing owner/);
|
||||
).rejects.toThrow(/missing workspace/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.com?owner=owner' },
|
||||
input: { repoUrl: 'bitbucket.org?type=bitbucket&workspace=workspace&repo=repo' },
|
||||
}),
|
||||
).rejects.toThrow(/missing project/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.org?type=bitbucket&workspace=workspace&project=project' },
|
||||
}),
|
||||
).rejects.toThrow(/missing repo/);
|
||||
});
|
||||
@@ -86,7 +93,7 @@ describe('publish:bitbucket', () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'missing.com?repo=bob&owner=owner' },
|
||||
input: { repoUrl: 'missing.com?type=bitbucket&workspace=workspace&project=project&repo=repo' },
|
||||
}),
|
||||
).rejects.toThrow(/No matching integration configuration/);
|
||||
});
|
||||
@@ -96,7 +103,7 @@ describe('publish:bitbucket', () => {
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'notoken.bitbucket.com?repo=bob&owner=owner',
|
||||
repoUrl: 'notoken.bitbucket.com?type=bitbucket&workspace=workspace&project=project&repo=repo',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/Authorization has not been provided for Bitbucket/);
|
||||
@@ -106,22 +113,22 @@ describe('publish:bitbucket', () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/owner/repo',
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer tokenlols');
|
||||
expect(req.body).toEqual({ is_private: true, scm: 'git' });
|
||||
expect(req.body).toEqual({ is_private: true, scm: 'git', project: { key: 'project' } });
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -138,7 +145,7 @@ describe('publish:bitbucket', () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos',
|
||||
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer thing');
|
||||
expect(req.body).toEqual({ public: false, name: 'repo' });
|
||||
@@ -169,7 +176,7 @@ describe('publish:bitbucket', () => {
|
||||
...mockContext,
|
||||
input: {
|
||||
...mockContext.input,
|
||||
repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo',
|
||||
repoUrl: 'hosted.bitbucket.com?type=bitbucket&project=project&repo=repo',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -259,7 +266,7 @@ describe('publish:bitbucket', () => {
|
||||
it('should call initAndPush with the correct values', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/owner/repo',
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
@@ -267,12 +274,12 @@ describe('publish:bitbucket', () => {
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/owner/cloneurl',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -475,7 +482,7 @@ describe('publish:bitbucket', () => {
|
||||
it('should call outputs with the correct urls', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/owner/repo',
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
@@ -483,12 +490,12 @@ describe('publish:bitbucket', () => {
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/owner/repo',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/owner/cloneurl',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -501,11 +508,11 @@ describe('publish:bitbucket', () => {
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://bitbucket.org/owner/cloneurl',
|
||||
'https://bitbucket.org/workspace/cloneurl',
|
||||
);
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'repoContentsUrl',
|
||||
'https://bitbucket.org/owner/repo/src/master',
|
||||
'https://bitbucket.org/workspace/repo/src/master',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,13 +26,14 @@ import { getRepoSourceDirectory, parseRepoUrl } from './util';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
const createBitbucketCloudRepository = async (opts: {
|
||||
owner: string;
|
||||
workspace: string;
|
||||
project: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
authorization: string;
|
||||
}) => {
|
||||
const { owner, repo, description, repoVisibility, authorization } = opts;
|
||||
const { workspace, project, repo, description, repoVisibility, authorization } = opts;
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
@@ -40,6 +41,7 @@ const createBitbucketCloudRepository = async (opts: {
|
||||
scm: 'git',
|
||||
description: description,
|
||||
is_private: repoVisibility === 'private',
|
||||
project: { key: project },
|
||||
}),
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
@@ -50,7 +52,7 @@ const createBitbucketCloudRepository = async (opts: {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(
|
||||
`https://api.bitbucket.org/2.0/repositories/${owner}/${repo}`,
|
||||
`https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`,
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -80,7 +82,7 @@ const createBitbucketCloudRepository = async (opts: {
|
||||
|
||||
const createBitbucketServerRepository = async (opts: {
|
||||
host: string;
|
||||
owner: string;
|
||||
project: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
@@ -89,7 +91,7 @@ const createBitbucketServerRepository = async (opts: {
|
||||
}) => {
|
||||
const {
|
||||
host,
|
||||
owner,
|
||||
project,
|
||||
repo,
|
||||
description,
|
||||
authorization,
|
||||
@@ -258,7 +260,23 @@ export function createPublishBitbucketAction(options: {
|
||||
enableLFS = false,
|
||||
} = ctx.input;
|
||||
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl);
|
||||
const { workspace, project, repo, host } = parseRepoUrl(repoUrl);
|
||||
|
||||
// Workspace is only required for bitbucket cloud
|
||||
if (host === 'bitbucket.org') {
|
||||
if (!workspace) {
|
||||
throw new InputError(
|
||||
`Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Project is required for both bitbucket cloud and bitbucket server
|
||||
if (!project) {
|
||||
throw new InputError(
|
||||
`Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`,
|
||||
);
|
||||
}
|
||||
|
||||
const integrationConfig = integrations.bitbucket.byHost(host);
|
||||
|
||||
@@ -272,14 +290,15 @@ export function createPublishBitbucketAction(options: {
|
||||
const apiBaseUrl = integrationConfig.config.apiBaseUrl;
|
||||
|
||||
const createMethod =
|
||||
host === 'bitbucket.org'
|
||||
? createBitbucketCloudRepository
|
||||
: createBitbucketServerRepository;
|
||||
host === 'bitbucket.org'
|
||||
? createBitbucketCloudRepository
|
||||
: createBitbucketServerRepository;
|
||||
|
||||
const { remoteUrl, repoContentsUrl } = await createMethod({
|
||||
authorization,
|
||||
host,
|
||||
owner,
|
||||
workspace: workspace || '',
|
||||
project,
|
||||
repo,
|
||||
repoVisibility,
|
||||
description,
|
||||
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
jest.mock('../../../stages/publish/helpers');
|
||||
|
||||
import { createPublishBitbucketCloudAction } from './bitbucketCloud';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { PassThrough } from 'stream';
|
||||
import { initRepoAndPush } from '../../../stages/publish/helpers';
|
||||
|
||||
describe('publish:bitbucketcloud', () => {
|
||||
const integrations = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
token: 'tokenlols',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const action = createPublishBitbucketCloudAction({ integrations });
|
||||
const mockContext = {
|
||||
input: {
|
||||
repoUrl: 'bitbucket.org?repo=repo&workspace=workspace&project=project',
|
||||
repoVisibility: 'private',
|
||||
},
|
||||
workspacePath: 'lol',
|
||||
logger: getVoidLogger(),
|
||||
logStream: new PassThrough(),
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
};
|
||||
const server = setupServer();
|
||||
msw.setupDefaultHandlers(server);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should throw an error when the repoUrl is not well formed', async () => {
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.org?repo=bob&workspace=workspace1' },
|
||||
}),
|
||||
).rejects.toThrow(/missing project/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.org?repo=bob&project=project1' },
|
||||
}),
|
||||
).rejects.toThrow(/missing workspace/);
|
||||
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'bitbucket.org?workspace=workspace1&project=project1' },
|
||||
}),
|
||||
).rejects.toThrow(/missing repo/);
|
||||
});
|
||||
|
||||
it('should call the correct APIs when the host is bitbucket cloud', async () => {
|
||||
expect.assertions(2);
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(req, res, ctx) => {
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer tokenlols');
|
||||
expect(req.body).toEqual({ is_private: true, scm: 'git', project: { key: 'project' } });
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(mockContext);
|
||||
});
|
||||
|
||||
it('should call initAndPush with the correct values', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(initRepoAndPush).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
|
||||
auth: { username: 'x-token-auth', password: 'tokenlols' },
|
||||
logger: mockContext.logger,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call outputs with the correct urls', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
links: {
|
||||
html: {
|
||||
href: 'https://bitbucket.org/workspace/repo',
|
||||
},
|
||||
clone: [
|
||||
{
|
||||
name: 'https',
|
||||
href: 'https://bitbucket.org/workspace/cloneurl',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://bitbucket.org/workspace/cloneurl',
|
||||
);
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'repoContentsUrl',
|
||||
'https://bitbucket.org/workspace/repo/src/master',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,198 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
BitbucketIntegrationConfig,
|
||||
ScmIntegrationRegistry,
|
||||
} from '@backstage/integration';
|
||||
import { initRepoAndPush } from '../../../stages/publish/helpers';
|
||||
import { getRepoSourceDirectory, parseRepoUrlForBitbucket } from './util';
|
||||
import fetch from 'cross-fetch';
|
||||
import { createTemplateAction } from '../../createTemplateAction';
|
||||
|
||||
const createBitbucketCloudRepository = async (opts: {
|
||||
workspace: string;
|
||||
project: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
authorization: string;
|
||||
}) => {
|
||||
const { workspace, project, repo, description, repoVisibility, authorization } = opts;
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
scm: 'git',
|
||||
description: description,
|
||||
is_private: repoVisibility === 'private',
|
||||
project: { key: project },
|
||||
}),
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(
|
||||
`https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`,
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to create repository, ${e}`);
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(
|
||||
`Unable to create repository, ${response.status} ${
|
||||
response.statusText
|
||||
}, ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const r = await response.json();
|
||||
let remoteUrl = '';
|
||||
for (const link of r.links.clone) {
|
||||
if (link.name === 'https') {
|
||||
remoteUrl = link.href;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO use the urlReader to get the default branch
|
||||
const repoContentsUrl = `${r.links.html.href}/src/master`;
|
||||
return { remoteUrl, repoContentsUrl };
|
||||
};
|
||||
|
||||
const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => {
|
||||
if (config.username && config.appPassword) {
|
||||
const buffer = Buffer.from(
|
||||
`${config.username}:${config.appPassword}`,
|
||||
'utf8',
|
||||
);
|
||||
|
||||
return `Basic ${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
if (config.token) {
|
||||
return `Bearer ${config.token}`;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Authorization has not been provided for Bitbucket. Please add either username + appPassword or token to the Integrations config`,
|
||||
);
|
||||
};
|
||||
|
||||
export function createPublishBitbucketCloudAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
}) {
|
||||
const { integrations } = options;
|
||||
|
||||
return createTemplateAction<{
|
||||
repoUrl: string;
|
||||
description: string;
|
||||
repoVisibility: 'private' | 'public';
|
||||
sourcePath?: string;
|
||||
}>({
|
||||
id: 'publish:bitbucketcloud',
|
||||
description:
|
||||
'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket.',
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['repoUrl'],
|
||||
properties: {
|
||||
repoUrl: {
|
||||
title: 'Repository Location',
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
title: 'Repository Description',
|
||||
type: 'string',
|
||||
},
|
||||
repoVisibility: {
|
||||
title: 'Repository Visiblity',
|
||||
type: 'string',
|
||||
enum: ['private', 'public'],
|
||||
},
|
||||
sourcePath: {
|
||||
title:
|
||||
'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the respository.',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteUrl: {
|
||||
title: 'A URL to the repository with the provider',
|
||||
type: 'string',
|
||||
},
|
||||
repoContentsUrl: {
|
||||
title: 'A URL to the root of the repository',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { repoUrl, description, repoVisibility = 'private' } = ctx.input;
|
||||
|
||||
const { workspace, project, repo, host } = parseRepoUrlForBitbucket(repoUrl);
|
||||
|
||||
const integrationConfig = integrations.bitbucket.byHost(host);
|
||||
|
||||
if (!integrationConfig) {
|
||||
throw new InputError(
|
||||
`No matching integration configuration for host ${host}, please check your integrations config`,
|
||||
);
|
||||
}
|
||||
|
||||
const authorization = getAuthorizationHeader(integrationConfig.config);
|
||||
|
||||
const createMethod = createBitbucketCloudRepository;
|
||||
|
||||
const { remoteUrl, repoContentsUrl } = await createMethod({
|
||||
authorization,
|
||||
workspace,
|
||||
project,
|
||||
repo,
|
||||
repoVisibility,
|
||||
description,
|
||||
});
|
||||
|
||||
await initRepoAndPush({
|
||||
dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
|
||||
remoteUrl,
|
||||
auth: {
|
||||
username: integrationConfig.config.username
|
||||
? integrationConfig.config.username
|
||||
: 'x-token-auth',
|
||||
password: integrationConfig.config.appPassword
|
||||
? integrationConfig.config.appPassword
|
||||
: integrationConfig.config.token ?? '',
|
||||
},
|
||||
logger: ctx.logger,
|
||||
});
|
||||
|
||||
ctx.output('remoteUrl', remoteUrl);
|
||||
ctx.output('repoContentsUrl', repoContentsUrl);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
export { createPublishAzureAction } from './azure';
|
||||
export { createPublishBitbucketAction } from './bitbucket';
|
||||
export { createPublishBitbucketCloudAction } from './bitbucketCloud';
|
||||
export { createPublishFileAction } from './file';
|
||||
export { createPublishGithubAction } from './github';
|
||||
export { createPublishGithubPullRequestAction } from './githubPullRequest';
|
||||
|
||||
@@ -47,19 +47,12 @@ export const parseRepoUrl = (repoUrl: string): RepoSpec => {
|
||||
);
|
||||
}
|
||||
const host = parsed.host;
|
||||
const type = parsed.searchParams.get('type');
|
||||
const owner = parsed.searchParams.get('owner');
|
||||
const workspace = parsed.searchParams.get('workspace');
|
||||
const project = parsed.searchParams.get('project');
|
||||
|
||||
if (!owner) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing owner`,
|
||||
);
|
||||
}
|
||||
const repo = parsed.searchParams.get('repo');
|
||||
if (!repo) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing repo`,
|
||||
);
|
||||
}
|
||||
if (type === 'bitbucket') {
|
||||
|
||||
const organization = parsed.searchParams.get('organization') ?? undefined;
|
||||
|
||||
@@ -84,23 +77,30 @@ export const parseRepoUrlForBitbucket = (repoUrl: string) => {
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing workspace`,
|
||||
);
|
||||
}
|
||||
|
||||
const project = parsed.searchParams.get('project');
|
||||
if (!project) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing project`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const repo = parsed.searchParams.get('repo');
|
||||
if (!repo) {
|
||||
else {
|
||||
if (!owner) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing repo`,
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing owner`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const organization = parsed.searchParams.get('organization');
|
||||
const repo = parsed.searchParams.get('repo');
|
||||
if (!repo) {
|
||||
throw new InputError(
|
||||
`Invalid repo URL passed to publisher: ${repoUrl}, missing repo`,
|
||||
);
|
||||
}
|
||||
|
||||
return { host, workspace, project, repo, organization };
|
||||
const organization = parsed.searchParams.get('organization');
|
||||
|
||||
return { host, owner, repo, organization, workspace, project };
|
||||
};
|
||||
|
||||
|
||||
@@ -79,10 +79,6 @@ export interface ScaffolderApi {
|
||||
allowedHosts: string[];
|
||||
}): Promise<{ type: string; title: string; host: string }[]>;
|
||||
|
||||
getIntegration(options: {
|
||||
type: string;
|
||||
}): Promise<AzureIntegration[] | BitbucketIntegration[] | GitHubIntegration[] | GitLabIntegration[] | undefined>;
|
||||
|
||||
// Returns a list of all installed actions.
|
||||
listActions(): Promise<ListActionsResponse>;
|
||||
|
||||
@@ -121,23 +117,6 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
.filter(c => options.allowedHosts.includes(c.host));
|
||||
}
|
||||
|
||||
async getIntegration(options: { type: string }) {
|
||||
switch (options.type) {
|
||||
case 'azure':
|
||||
return this.scmIntegrationsApi.azure.list()
|
||||
case 'bitbucket':
|
||||
return this.scmIntegrationsApi.bitbucket.list()
|
||||
case 'github':
|
||||
return this.scmIntegrationsApi.github.list()
|
||||
case 'gitlab':
|
||||
return this.scmIntegrationsApi.gitlab.list()
|
||||
default:
|
||||
throw new Error(
|
||||
`No integration found for ${options.type}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getTemplateParameterSchema(
|
||||
templateName: EntityName,
|
||||
): Promise<TemplateParameterSchema> {
|
||||
|
||||
@@ -24,7 +24,6 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
scaffold: jest.fn(),
|
||||
getTemplateParameterSchema: jest.fn(),
|
||||
getIntegrationsList: jest.fn(),
|
||||
getIntegration: jest.fn(),
|
||||
getTask: jest.fn(),
|
||||
streamLogs: jest.fn(),
|
||||
listActions: jest.fn(),
|
||||
|
||||
@@ -40,7 +40,6 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
scaffold: jest.fn(),
|
||||
getTemplateParameterSchema: jest.fn(),
|
||||
getIntegrationsList: jest.fn(),
|
||||
getIntegration: jest.fn(),
|
||||
getTask: jest.fn(),
|
||||
streamLogs: jest.fn(),
|
||||
listActions: jest.fn(),
|
||||
|
||||
@@ -28,36 +28,50 @@ import { Progress } from '@backstage/core-components';
|
||||
|
||||
function splitFormData(url: string | undefined) {
|
||||
let host = undefined;
|
||||
let type = undefined;
|
||||
let owner = undefined;
|
||||
let repo = undefined;
|
||||
let organization = undefined;
|
||||
let workspace = undefined;
|
||||
let project = undefined;
|
||||
|
||||
try {
|
||||
if (url) {
|
||||
const parsed = new URL(`https://${url}`);
|
||||
host = parsed.host;
|
||||
type = parsed.searchParams.get('type') || undefined;
|
||||
owner = parsed.searchParams.get('owner') || undefined;
|
||||
repo = parsed.searchParams.get('repo') || undefined;
|
||||
// This is azure dev ops specific. not used for any other provider.
|
||||
organization = parsed.searchParams.get('organization') || undefined;
|
||||
// These are bitbucket specific, not used for any other provider.
|
||||
workspace = parsed.searchParams.get('workspace') || undefined;
|
||||
project = parsed.searchParams.get('project') || undefined;
|
||||
}
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
|
||||
return { host, owner, repo, organization };
|
||||
return { host, type, owner, repo, organization, workspace, project };
|
||||
}
|
||||
|
||||
function serializeFormData(data: {
|
||||
host?: string;
|
||||
type?: string;
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
organization?: string;
|
||||
workspace?: string;
|
||||
project?: string;
|
||||
}) {
|
||||
if (!data.host) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (data.type) {
|
||||
params.set('type', data.type);
|
||||
}
|
||||
if (data.owner) {
|
||||
params.set('owner', data.owner);
|
||||
}
|
||||
@@ -67,6 +81,12 @@ function serializeFormData(data: {
|
||||
if (data.organization) {
|
||||
params.set('organization', data.organization);
|
||||
}
|
||||
if (data.workspace) {
|
||||
params.set('workspace', data.workspace);
|
||||
}
|
||||
if (data.project) {
|
||||
params.set('project', data.project);
|
||||
}
|
||||
|
||||
return `${data.host}?${params.toString()}`;
|
||||
}
|
||||
@@ -84,18 +104,22 @@ export const RepoUrlPicker = ({
|
||||
return await api.getIntegrationsList({ allowedHosts });
|
||||
});
|
||||
|
||||
const { host, owner, repo, organization } = splitFormData(formData);
|
||||
const { host, type, owner, repo, organization, workspace, project } = splitFormData(formData);
|
||||
const updateHost = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) => {
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host: evt.target.value as string,
|
||||
type: (integrations?getIntegrationTypeByHost(evt.target.value as string, integrations): undefined),
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, owner, repo, organization],
|
||||
)
|
||||
},
|
||||
[onChange, integrations, owner, repo, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateOwner = useCallback(
|
||||
@@ -103,12 +127,15 @@ export const RepoUrlPicker = ({
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
type,
|
||||
owner: evt.target.value as string,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, repo, organization],
|
||||
[onChange, host, type, repo, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateRepo = useCallback(
|
||||
@@ -116,12 +143,15 @@ export const RepoUrlPicker = ({
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
type,
|
||||
owner,
|
||||
repo: evt.target.value as string,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, organization],
|
||||
[onChange, host, type, owner, organization, workspace, project],
|
||||
);
|
||||
|
||||
const updateOrganization = useCallback(
|
||||
@@ -129,12 +159,47 @@ export const RepoUrlPicker = ({
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
type,
|
||||
owner,
|
||||
repo,
|
||||
organization: evt.target.value as string,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, owner, repo],
|
||||
[onChange, host , type, owner, repo, workspace, project],
|
||||
);
|
||||
|
||||
const updateWorkspace = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
type,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace: evt.target.value as string,
|
||||
project,
|
||||
}),
|
||||
),
|
||||
[onChange, host, type, owner, repo, organization, project],
|
||||
);
|
||||
|
||||
const updateProject = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
type,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project: evt.target.value as string,
|
||||
}),
|
||||
),
|
||||
[onChange, host, type, owner, repo, organization, workspace],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -142,13 +207,16 @@ export const RepoUrlPicker = ({
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host: integrations[0].host,
|
||||
type: integrations[0].type,
|
||||
owner,
|
||||
repo,
|
||||
organization,
|
||||
workspace,
|
||||
project,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [onChange, integrations, host, owner, repo, organization]);
|
||||
}, [onChange, integrations, host, type, owner, repo, organization, workspace, project]);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
@@ -179,6 +247,7 @@ export const RepoUrlPicker = ({
|
||||
The host where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
{/* Show this for dev.azure.com only */}
|
||||
{host === 'dev.azure.com' && (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
@@ -194,17 +263,52 @@ export const RepoUrlPicker = ({
|
||||
<FormHelperText>The name of the organization</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
<InputLabel htmlFor="ownerInput">Owner</InputLabel>
|
||||
<Input id="ownerInput" onChange={updateOwner} value={owner} />
|
||||
<FormHelperText>
|
||||
The organization, user or project that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
{/* Show this for bitbucket.org only */}
|
||||
{type === 'bitbucket' && (
|
||||
<>
|
||||
{host === 'bitbucket.org' && (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !workspace}
|
||||
>
|
||||
<InputLabel htmlFor="wokrspaceInput">Workspace</InputLabel>
|
||||
<Input id="wokrspaceInput" onChange={updateWorkspace} value={workspace} />
|
||||
<FormHelperText>
|
||||
The workspace where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !project}
|
||||
>
|
||||
<InputLabel htmlFor="wokrspaceInput">Project</InputLabel>
|
||||
<Input id="wokrspaceInput" onChange={updateProject} value={project} />
|
||||
<FormHelperText>
|
||||
The project where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
{/* Show this for all hosts except bitbucket */}
|
||||
{type !== 'bitbucket' && (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !owner}
|
||||
>
|
||||
<InputLabel htmlFor="ownerInput">Owner</InputLabel>
|
||||
<Input id="ownerInput" onChange={updateOwner} value={owner} />
|
||||
<FormHelperText>
|
||||
The organization, user or project that this repo will belong to
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
{/* Show this for all hosts */}
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
|
||||
-239
@@ -1,239 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 React, { useCallback, useEffect } from 'react';
|
||||
import { Field } from '@rjsf/core';
|
||||
import { useApi, Progress } from '@backstage/core';
|
||||
import { scaffolderApiRef } from '../../../api';
|
||||
import { useAsync } from 'react-use';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import { BitbucketIntegration } from '@backstage/integration';
|
||||
|
||||
function splitFormData(url: string | undefined) {
|
||||
let host = undefined;
|
||||
let workspace = undefined;
|
||||
let project = undefined;
|
||||
let repo = undefined;
|
||||
|
||||
try {
|
||||
if (url) {
|
||||
const parsed = new URL(`https://${url}`);
|
||||
host = parsed.host;
|
||||
workspace = parsed.searchParams.get('workspace') || undefined;
|
||||
project = parsed.searchParams.get('project') || undefined;
|
||||
repo = parsed.searchParams.get('repo') || undefined;
|
||||
}
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
|
||||
return { host, workspace, project, repo };
|
||||
}
|
||||
|
||||
function serializeFormData(data: {
|
||||
host?: string;
|
||||
workspace?: string;
|
||||
project?: string;
|
||||
repo?: string;
|
||||
}) {
|
||||
if (!data.host) {
|
||||
return undefined;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
if (data.workspace) {
|
||||
params.set('workspace', data.workspace);
|
||||
}
|
||||
if (data.project) {
|
||||
params.set('project', data.project);
|
||||
}
|
||||
if (data.repo) {
|
||||
params.set('repo', data.repo);
|
||||
}
|
||||
|
||||
return `${data.host}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export const RepoUrlPickerBitbucketCloud: Field = ({
|
||||
onChange,
|
||||
rawErrors,
|
||||
formData,
|
||||
}) => {
|
||||
|
||||
const api = useApi(scaffolderApiRef);
|
||||
const allowedHosts: string[] = ['bitbucket.org'];
|
||||
|
||||
const { value: integrations, loading: loadingIntegrations } = useAsync(async () => {
|
||||
return await api.getIntegrationsList({ allowedHosts });
|
||||
});
|
||||
|
||||
const { value: data, loading: loadingData } = useAsync(async () => {
|
||||
const bitbucketIntegrations: BitbucketIntegration[] = await api.getIntegration({ type: 'bitbucket' }) as BitbucketIntegration[];
|
||||
const bitbucketIntegration = bitbucketIntegrations.filter(element => element.config.host === 'bitbucket.org')[0];
|
||||
const workspaces = bitbucketIntegration.config.workspaces;
|
||||
const projects = workspaces?.[0]?.projects;
|
||||
|
||||
return { workspaces, projects };
|
||||
});
|
||||
|
||||
const { host, workspace, project, repo } = splitFormData(formData);
|
||||
const updateHost = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host: evt.target.value as string,
|
||||
workspace,
|
||||
project,
|
||||
repo,
|
||||
}),
|
||||
),
|
||||
[onChange, workspace, project, repo],
|
||||
);
|
||||
|
||||
const updateWorkspace = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) => {
|
||||
if (data) {
|
||||
data.projects = data?.workspaces?.find(w => w.name === evt.target.value)?.projects;
|
||||
}
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
workspace: evt.target.value as string,
|
||||
project,
|
||||
repo,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[onChange, data, host, project, repo],
|
||||
);
|
||||
|
||||
const updateProject = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) => {
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
workspace,
|
||||
project: evt.target.value as string,
|
||||
repo,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[onChange, host, workspace, repo],
|
||||
);
|
||||
|
||||
const updateRepo = useCallback(
|
||||
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host,
|
||||
workspace,
|
||||
project,
|
||||
repo: evt.target.value as string,
|
||||
}),
|
||||
),
|
||||
[onChange, host, workspace, project],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(
|
||||
serializeFormData({
|
||||
host: (host === undefined ? (integrations?.[0]?.host) : undefined),
|
||||
workspace: workspace === undefined ? (data?.workspaces?.[0]?.name) : undefined,
|
||||
project: project === undefined ? (data?.workspaces?.[0]?.projects?.[0]?.key) : undefined,
|
||||
repo,
|
||||
}),
|
||||
);
|
||||
}, [onChange, integrations, data, host, workspace, project, repo]);
|
||||
|
||||
if (loadingIntegrations || loadingData) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !host}
|
||||
>
|
||||
<InputLabel htmlFor="hostInput">Host</InputLabel>
|
||||
<Select native id="hostInput" onChange={updateHost} value={host}>
|
||||
{integrations ? (
|
||||
integrations
|
||||
.filter(i => allowedHosts?.includes(i.host))
|
||||
.map(i => (
|
||||
<option key={i.host} value={i.host}>
|
||||
{i.title}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<p>loading</p>
|
||||
)}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
The host where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !workspace}
|
||||
>
|
||||
<InputLabel htmlFor="workspaceInput">Workspace</InputLabel>
|
||||
<Select native id="workspaceInput" onChange={updateWorkspace} value={workspace}>
|
||||
{data?.workspaces? (
|
||||
data.workspaces.map((w, index) => (
|
||||
<option key={index} value={w.name}>{w.description?w.description:w.name}</option>
|
||||
))
|
||||
) : (
|
||||
<p>loading</p>
|
||||
)}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
The workspace where the repository will be created
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !project}
|
||||
>
|
||||
<InputLabel htmlFor="projectInput">Project</InputLabel>
|
||||
<Select native id="projectInput" onChange={updateProject} value={project}>
|
||||
{data?.projects ? (
|
||||
data.projects.map((p, index) => (
|
||||
<option key={index} value={p.key}>{p.description?p.description:p.name}</option>
|
||||
))
|
||||
) : (
|
||||
<p>loading</p>
|
||||
)}
|
||||
</Select>
|
||||
<FormHelperText>The bitbucket project this repo should be added to</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required
|
||||
error={rawErrors?.length > 0 && !repo}
|
||||
>
|
||||
<InputLabel htmlFor="repoInput">Repository</InputLabel>
|
||||
<Input id="repoInput" onChange={updateRepo} value={repo} />
|
||||
<FormHelperText>The name of the repository</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user