feat: starting to add the RepoUrlPicker with integrations config

Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
blam
2021-02-23 15:27:35 +01:00
committed by Johan Haals
parent 0d44305a4e
commit 59769bad27
9 changed files with 312 additions and 65 deletions
+3
View File
@@ -248,6 +248,9 @@ catalog:
- type: file
target: ../catalog-model/examples/acme-corp.yaml
- type: file
target: ../../plugins/scaffolder-backend/sample-templates/test-template-v2/template.yaml
scaffolder:
github:
token:
@@ -1,63 +1,63 @@
apiVersion: backstage.io/v1beta2
kind: Template
metadata:
name: test-template
title: Test Template
description: Testing out the new template schema
spec:
owner: backstage/techdocs-core
type: service
# apiVersion: backstage.io/v1beta2
# kind: Template
# metadata:
# name: test-template
# title: Test Template
# description: Testing out the new template schema
# spec:
# owner: backstage/techdocs-core
# type: service
parameters:
title: Fill in some BS params
required:
- name
properties:
name:
title: Name
type: string
description: Unique name of the component
ui:widget: textarea
ui:autofocus: true
ui:options:
rows: 5
storePath: # provides {owner, repository, providerUrl}
# $ref: '#/definitions/StorePath'
title: Store Path
type: string
description: Copy https://github.com/aeothgiaetgh/aetkijhahte and hammer in some more letters
# parameters:
# title: Fill in some BS params
# required:
# - name
# properties:
# name:
# title: Name
# type: string
# description: Unique name of the component
# ui:widget: textarea
# ui:autofocus: true
# ui:options:
# rows: 5
# storePath: # provides {owner, repository, providerUrl}
# # $ref: '#/definitions/StorePath'
# title: Store Path
# type: string
# description: Copy https://github.com/aeothgiaetgh/aetkijhahte and hammer in some more letters
steps:
- id: prepare
name: Prepare
action: legacy:prepare
parameters:
protocol: file
url: file:///Users/patriko/dev/backstage/plugins/scaffolder-backend/sample-templates/test-template-v2
- id: template
name: Template
action: legacy:template
parameters:
templater: cookiecutter
values:
name: '{{ parameters.name }}'
- id: publish
name: Publish
action: legacy:publish
parameters:
values:
name: '{{ parameters.name }}'
storePath: '{{ parameters.storePath }}'
owner: kungen # not use
- id: register
name: Register
action: catalog:register
parameters:
catalogInfoUrl: '{{ steps.publish.output.catalogInfoUrl }}'
# steps:
# - id: prepare
# name: Prepare
# action: legacy:prepare
# parameters:
# protocol: file
# url: file:///Users/patriko/dev/backstage/plugins/scaffolder-backend/sample-templates/test-template-v2
# - id: template
# name: Template
# action: legacy:template
# parameters:
# templater: cookiecutter
# values:
# name: '{{ parameters.name }}'
# - id: publish
# name: Publish
# action: legacy:publish
# parameters:
# values:
# name: '{{ parameters.name }}'
# storePath: '{{ parameters.storePath }}'
# owner: kungen # not use
# - id: register
# name: Register
# action: catalog:register
# parameters:
# catalogInfoUrl: '{{ steps.publish.output.catalogInfoUrl }}'
output:
catalogInfoUrl: '{{ steps.publish.output.catalogInfoUrl }}'
entityRef: '{{ steps.register.entityRef }}'
# output:
# catalogInfoUrl: '{{ steps.publish.output.catalogInfoUrl }}'
# entityRef: '{{ steps.register.entityRef }}'
# TODO:
@@ -90,15 +90,17 @@ spec:
title: Name
type: string
description: Unique name of the component
ui:widget: textarea
ui:autofocus: true
ui:options:
rows: 5
storePath: # provides {owner, repository, providerUrl}
# $ref: '#/definitions/StorePath'
title: Store Path
repoUrl:
title: Repository Location
type: string
description: Copy https://github.com/aeothgiaetgh/aetkijhahte and hammer in some more letters
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
steps:
- id: fetch-base
@@ -115,13 +117,16 @@ spec:
action: fetch:plain
parameters:
url: https://github.com/aeothgiaetgh/aetkijhahte/tree/master/docs/template
- id: publish
name: Publish
action: publish
action: publish:github
parameters:
allowedHosts: ['ghe.mycompany.com']
name: '{{ parameters.name }}'
# storePath: github.com?owner=spotify&repo=name
storePath: '{{ parameters.storePath }}'
- id: register
name: Register
action: catalog:register
+24
View File
@@ -20,8 +20,10 @@ import {
createApiRef,
DiscoveryApi,
Observable,
ConfigApi,
IdentityApi,
} from '@backstage/core';
import { ScmIntegrations } from '@backstage/integration';
import ObservableImpl from 'zen-observable';
import { ScaffolderTask, Status } from './types';
@@ -66,6 +68,10 @@ export interface ScaffolderApi {
getTask(taskId: string): Promise<ScaffolderTask>;
getIntegrationsList(options: {
allowedHosts: string[];
}): Promise<{ type: string; title: string; host: string }[]>;
streamLogs({
taskId,
after,
@@ -77,13 +83,31 @@ export interface ScaffolderApi {
export class ScaffolderClient implements ScaffolderApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly configApi: ConfigApi;
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
configApi: ConfigApi;
}) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
this.configApi = options.configApi;
}
async getIntegrationsList(options: { allowedHosts: string[] }) {
const integrations = ScmIntegrations.fromConfig(
this.configApi.getConfig('integrations'),
);
return [
...integrations.azure.list(),
...integrations.bitbucket.list(),
...integrations.github.list(),
...integrations.gitlab.list(),
]
.map(c => ({ type: c.type, title: c.title, host: c.config.host }))
.filter(c => options.allowedHosts.includes(c.host));
}
async getTemplateParameterSchema(
@@ -45,6 +45,8 @@ type Props = {
onChange: (e: IChangeEvent) => void;
onReset: () => void;
onFinish: () => void;
widgets?: FormProps<any>['widgets'];
fields?: FormProps<any>['fields'];
};
export const MultistepJsonForm = ({
@@ -53,6 +55,8 @@ export const MultistepJsonForm = ({
onChange,
onReset,
onFinish,
fields,
widgets,
}: Props) => {
const [activeStep, setActiveStep] = useState(0);
@@ -77,6 +81,8 @@ export const MultistepJsonForm = ({
noHtml5Validate
formData={formData}
onChange={onChange}
fields={fields}
widgets={widgets}
onSubmit={e => {
if (e.errors.length === 0) handleNext();
}}
@@ -33,6 +33,7 @@ import { useAsync } from 'react-use';
import { scaffolderApiRef } from '../../api';
import { rootRouteRef } from '../../routes';
import { MultistepJsonForm } from '../MultistepJsonForm';
import { RepoUrlPicker } from '../fields';
const useTemplateParameterSchema = (templateName: string) => {
const scaffolderApi = useApi(scaffolderApiRef);
@@ -53,7 +54,9 @@ const storePathValidator = (
errors: FormValidation,
) => {
const { storePath } = formData;
if (!storePath) {
errors.storePath.addError('Store path is required and not present');
return errors;
}
@@ -131,6 +134,7 @@ export const TemplatePage = () => {
<InfoCard title={schema.title} noPadding>
<MultistepJsonForm
formData={formState}
fields={{ RepoUrlPicker }}
onChange={handleChange}
onReset={handleFormReset}
onFinish={handleCreate}
@@ -0,0 +1,95 @@
/*
* 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, { useState, useCallback, useEffect } from 'react';
import { Field, Widget } from '@rjsf/core';
import { useApi, Progress } from '@backstage/core';
import { scaffolderApiRef } from '../../../api';
import { useAsync } from 'react-use';
import TextField from '@material-ui/core/TextField';
import MenuItem from '@material-ui/core/MenuItem';
import { Typography } from '@material-ui/core';
export const RepoUrlPicker: Field = ({ onChange, uiSchema }) => {
const api = useApi(scaffolderApiRef);
const allowedHosts = uiSchema['ui:options']?.allowedHosts as string[];
const { value: integrations, loading } = useAsync(async () => {
return await api.getIntegrationsList({ allowedHosts });
});
const [hostname, setHostname] = useState('');
const [owner, setOwner] = useState('');
const [repo, setRepo] = useState('');
const updateHostname = useCallback(
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
setHostname(evt.target.value as string),
[setHostname],
);
const updateOwner = useCallback(
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
setOwner(evt.target.value as string),
[setOwner],
);
const updateRepo = useCallback(
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
setRepo(evt.target.value as string),
[setRepo],
);
useEffect(() => {
if (hostname === '' && integrations?.length) {
setHostname(integrations[0].host);
}
}, [integrations, hostname]);
useEffect(() => {
const params = new URLSearchParams();
params.set('owner', owner);
params.set('repo', repo);
onChange(`${hostname}?${params.toString()}`);
}, [hostname, owner, repo, onChange]);
if (loading) {
return <Progress />;
}
return (
<>
<Typography>Repository Location</Typography>
<TextField
select
label={hostname ? '' : 'Hostname'}
value={hostname}
onChange={updateHostname}
>
{integrations!
.filter(i => allowedHosts?.includes(i.host))
.map(({ host, title }) => (
<MenuItem key={host} value={host}>
{title}
</MenuItem>
))}
</TextField>
<TextField label="Owner" onBlur={updateOwner} />
<TextField label="Repository name" onBlur={updateRepo} />
</>
);
};
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { RepoUrlPicker } from './RepoUrlPicker';
@@ -0,0 +1,89 @@
/*
* Cop
yright 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, { useState, useCallback, useEffect } from 'react';
import { Field, Widget } from '@rjsf/core';
import { useApi, Progress } from '@backstage/core';
import { scaffolderApiRef } from '../../../api';
import { useAsync } from 'react-use';
import TextField from '@material-ui/core/TextField';
import MenuItem from '@material-ui/core/MenuItem';
import InputLabel from '@material-ui/core/InputLabel';
import { Typography } from '@material-ui/core';
import { rest } from 'msw/lib/types';
export const StorePathPicker: Field = ({
options,
onChange,
rawErrors,
formContext,
uiSchema
}) => {
const api = useApi(scaffolderApiRef);
console.log(uiSchema)
const allowedHosts = uiSchema['ui:Options'].allowedHosts as string[];
const { value: integrations, loading } = useAsync(async () => {
return await api.getIntegrationsList({ allowedHosts });
});
const [hostname, setHostname] = useState<string | undefined>('');
const [organization, setOrganization] = useState<string | undefined>('');
const [repositoryName, setRepositoryName] = useState<string | undefined>('');
const [organization, setOrganization] = useState<string | undefined>('');
const [repositoryName, setRepositoryName] = useState<string | undefined>('');
const updateHostname = useCallback(
(evt: React.ChangeEvent<{ name?: string; value: unknown }>) =>
setHostname(evt.target.value as string),
[setHostname],
);
useEffect(() => {
if (hostname === '' && integrations?.length) {
setHostname(integrations[0].host);
}
}, [integrations, hostname]);
if (loading) {
return <Progress />;
}
return (
<>
<Typography>Repository Location</Typography>
<TextField
select
label={hostname ? '' : 'Hostname'}
value={hostname}
onChange={updateHostname}
>
{integrations!
.filter(i => allowedHosts?.includes(i.host))
.map(({ host, title }) => (
<MenuItem key={host} value={host}>
{title}
</MenuItem>() =?> Setsets
))}
</TextField>
<TextField label="Organization" />
<TextField label="Repository name" />
</>
);
};
+8 -3
View File
@@ -19,6 +19,7 @@ import {
createApiFactory,
discoveryApiRef,
identityApiRef,
configApiRef,
createRoutableExtension,
} from '@backstage/core';
import { rootRouteRef } from './routes';
@@ -29,9 +30,13 @@ export const scaffolderPlugin = createPlugin({
apis: [
createApiFactory({
api: scaffolderApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) =>
new ScaffolderClient({ discoveryApi, identityApi }),
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, identityApi, configApi }) =>
new ScaffolderClient({ discoveryApi, identityApi, configApi }),
}),
],
routes: {