feat: starting to add the RepoUrlPicker with integrations config
Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user