Add OwnerPicker component to scaffolder

Signed-off-by: James Turley <jamesturley1905@googlemail.com>
This commit is contained in:
James Turley
2021-03-24 17:01:23 +00:00
committed by James Turley
parent ba479febd3
commit 2ab6f3ff07
9 changed files with 299 additions and 3 deletions
@@ -12,6 +12,7 @@ spec:
- title: Fill in some steps
required:
- name
- owner
properties:
name:
title: Name
@@ -20,6 +21,14 @@ spec:
ui:autofocus: true
ui:options:
rows: 5
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
- title: Choose a location
required:
- repoUrl
@@ -40,6 +49,7 @@ spec:
url: ./template
values:
name: '{{ parameters.name }}'
owner: '{{ parameters.owner }}'
- id: fetch-docs
name: Fetch Docs
@@ -5,4 +5,4 @@ metadata:
spec:
type: website
lifecycle: experimental
owner: guest
owner: {{cookiecutter.owner | jsonify}}
@@ -33,7 +33,7 @@ import { useAsync } from 'react-use';
import { scaffolderApiRef } from '../../api';
import { rootRouteRef } from '../../routes';
import { MultistepJsonForm } from '../MultistepJsonForm';
import { RepoUrlPicker } from '../fields';
import { RepoUrlPicker, OwnerPicker } from '../fields';
import { JsonObject } from '@backstage/config';
const useTemplateParameterSchema = (templateName: string) => {
@@ -190,7 +190,7 @@ export const TemplatePage = () => {
<InfoCard title={schema.title} noPadding>
<MultistepJsonForm
formData={formState}
fields={{ RepoUrlPicker }}
fields={{ RepoUrlPicker, OwnerPicker }}
onChange={handleChange}
onReset={handleFormReset}
onFinish={handleCreate}
@@ -0,0 +1,141 @@
/*
* Copyright 2020 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 from 'react';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import userEvent from '@testing-library/user-event';
import { Entity } from '@backstage/catalog-model';
import { FieldProps } from '@rjsf/core';
import { OwnerPicker } from './OwnerPicker';
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
apiVersion: 'backstage.io/v1beta1',
kind,
metadata: { namespace, name },
});
describe('<OwnerPicker />', () => {
let entities: Entity[];
const onChange = jest.fn();
const schema = {};
const required = false;
let uiSchema: { 'ui:options': { allowedKinds?: string[] } };
const rawErrors: string[] = [];
const formData = undefined;
let props: FieldProps;
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
getEntities: jest.fn(async () => ({ items: entities })),
addLocation: jest.fn(),
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
} as any;
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
entities = [
makeEntity('Group', 'default', 'team-a'),
makeEntity('Group', 'default', 'squad-b'),
];
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
);
});
afterEach(() => jest.resetAllMocks());
describe('without allowedKinds', () => {
beforeEach(() => {
uiSchema = { 'ui:options': {} };
props = ({
onChange,
schema,
required,
uiSchema,
rawErrors,
formData,
} as unknown) as FieldProps<any>;
catalogApi.getEntities.mockResolvedValue({ items: entities });
});
it('searches for users and groups', async () => {
await renderInTestApp(
<Wrapper>
<OwnerPicker {...props} />
</Wrapper>,
);
expect(catalogApi.getEntities).toHaveBeenCalledWith({
filter: {
kind: ['Group', 'User'],
},
});
});
it('updates even if there is not an exact match', async () => {
const { getByLabelText } = await renderInTestApp(
<Wrapper>
<OwnerPicker {...props} />
</Wrapper>,
);
const input = getByLabelText('Owner');
userEvent.type(input, 'squ');
input.blur();
expect(onChange).toHaveBeenCalledWith('squ');
});
});
describe('with allowedKinds', () => {
beforeEach(() => {
uiSchema = { 'ui:options': { allowedKinds: ['User'] } };
props = ({
onChange,
schema,
required,
uiSchema,
rawErrors,
formData,
} as unknown) as FieldProps<any>;
catalogApi.getEntities.mockResolvedValue({ items: entities });
});
it('searches for users and groups', async () => {
await renderInTestApp(
<Wrapper>
<OwnerPicker {...props} />
</Wrapper>,
);
expect(catalogApi.getEntities).toHaveBeenCalledWith({
filter: {
kind: ['User'],
},
});
});
});
});
@@ -0,0 +1,93 @@
/*
* 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 from 'react';
import { Field } from '@rjsf/core';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { useApi } from '@backstage/core';
import { useAsync } from 'react-use';
import Autocomplete from '@material-ui/lab/Autocomplete';
import FormControl from '@material-ui/core/FormControl';
import { Entity } from '@backstage/catalog-model';
import { TextField } from '@material-ui/core';
const entityRef = (entity: Entity | undefined): string => {
if (!entity) {
return '';
}
const {
kind,
metadata: { namespace, name },
} = entity;
const namespacePart =
!namespace || namespace === 'default' ? '' : `${namespace}/`;
const kindPart = kind.toLowerCase() === 'group' ? '' : `${kind}:`;
return `${kindPart}${namespacePart}${name}`;
};
export const OwnerPicker: Field = ({
onChange,
schema: { title = 'Owner', description = 'The owner of the component' },
required,
uiSchema,
rawErrors,
formData,
}) => {
const allowedKinds = (uiSchema['ui:options']?.allowedKinds || [
'Group',
'User',
]) as string[];
const catalogApi = useApi(catalogApiRef);
const { value: owners, loading } = useAsync(() =>
catalogApi.getEntities({ filter: { kind: allowedKinds } }),
);
const ownerRefs = owners?.items.map(entityRef);
const onSelect = (_: any, value: string | null) => {
onChange(value || '');
};
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
>
<Autocomplete
value={(formData as string) || ''}
loading={loading}
onChange={onSelect}
options={ownerRefs || []}
autoSelect
freeSolo
renderInput={params => (
<TextField
{...params}
label={title}
margin="normal"
helperText={description}
variant="outlined"
required={required}
InputProps={params.InputProps}
/>
)}
/>
</FormControl>
);
};
@@ -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 * from './OwnerPicker';
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export * from './RepoUrlPicker';
export * from './OwnerPicker';