Skip to content

Commit

Permalink
add OwnedEntityPicker field
Browse files Browse the repository at this point in the history
Signed-off-by: mufaddal motiwala <mufaddalmm.52@gmail.com>
  • Loading branch information
mufaddal7 committed Dec 9, 2021
1 parent d81b57f commit 0f645a7
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-buses-collect.md
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---

Added OwnedEntityPicker field which displays Owned Entities in options
@@ -0,0 +1,137 @@
/*
* Copyright 2021 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 { Entity } from '@backstage/catalog-model';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { FieldProps } from '@rjsf/core';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { OwnedEntityPicker } from './OwnedEntityPicker';

const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
apiVersion: 'backstage.io/v1beta1',
kind,
metadata: { namespace, name },
});

describe('<OwnedEntityPicker />', () => {
let entities: Entity[];
const onChange = jest.fn();
const schema = {};
const required = false;
let uiSchema: {
'ui:options': { allowedKinds?: string[]; defaultKind?: 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(() => {
entities = [
makeEntity('Group', 'default', 'team-a'),
makeEntity('Group', 'default', 'squad-b'),
];

Wrapper = ({ children }: { children?: React.ReactNode }) => (
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
</TestApiProvider>
);
});

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 all entities', async () => {
await renderInTestApp(
<Wrapper>
<OwnedEntityPicker {...props} />
</Wrapper>,
);

expect(catalogApi.getEntities).toHaveBeenCalledWith(undefined);
});

it('updates even if there is not an exact match', async () => {
const { getByLabelText } = await renderInTestApp(
<Wrapper>
<OwnedEntityPicker {...props} />
</Wrapper>,
);
const input = getByLabelText('Entity');

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>
<OwnedEntityPicker {...props} />
</Wrapper>,
);

expect(catalogApi.getEntities).toHaveBeenCalledWith({
filter: {
kind: ['User'],
},
});
});
});
});
@@ -0,0 +1,86 @@
/*
* Copyright 2021 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 { useApi } from '@backstage/core-plugin-api';
import {
catalogApiRef,
formatEntityRefTitle,
useEntityOwnership,
} from '@backstage/plugin-catalog-react';
import { TextField } from '@material-ui/core';
import FormControl from '@material-ui/core/FormControl';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { FieldProps } from '@rjsf/core';
import React from 'react';
import { useAsync } from 'react-use';

export const OwnedEntityPicker = ({
onChange,
schema: { title = 'Entity', description = 'An entity from the catalog' },
required,
uiSchema,
rawErrors,
formData,
idSchema,
}: FieldProps<string>) => {
const allowedKinds = uiSchema['ui:options']?.allowedKinds as string[];
const defaultKind = uiSchema['ui:options']?.defaultKind as string | undefined;
const catalogApi = useApi(catalogApiRef);
const { isOwnedEntity } = useEntityOwnership();

const { value: entities, loading } = useAsync(() =>
catalogApi.getEntities(
allowedKinds ? { filter: { kind: allowedKinds } } : undefined,
),
);

const entityRefs = entities?.items
.map(e =>
isOwnedEntity(e) ? formatEntityRefTitle(e, { defaultKind }) : null,
)
.filter(n => n);

const onSelect = (_: any, value: string | null) => {
onChange(value || '');
};
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
>
<Autocomplete
id={idSchema?.$id}
value={(formData as string) || ''}
loading={loading}
onChange={onSelect}
options={entityRefs || []}
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 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.
*/
export { OwnedEntityPicker } from './OwnedEntityPicker';
1 change: 1 addition & 0 deletions plugins/scaffolder/src/components/fields/index.ts
Expand Up @@ -18,3 +18,4 @@ export * from './EntityPicker';
export * from './OwnerPicker';
export * from './RepoUrlPicker';
export * from './TextValuePicker';
export * from './OwnedEntityPicker';
5 changes: 5 additions & 0 deletions plugins/scaffolder/src/extensions/default.ts
Expand Up @@ -24,6 +24,7 @@ import {
RepoUrlPicker,
} from '../components/fields/RepoUrlPicker';
import { FieldExtensionOptions } from './types';
import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker';

export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [
{
Expand All @@ -44,4 +45,8 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [
component: OwnerPicker,
name: 'OwnerPicker',
},
{
component: OwnedEntityPicker,
name: 'OwnedEntityPicker',
},
];
2 changes: 2 additions & 0 deletions plugins/scaffolder/src/index.ts
Expand Up @@ -31,6 +31,7 @@ export {
EntityPickerFieldExtension,
EntityNamePickerFieldExtension,
OwnerPickerFieldExtension,
OwnedEntityPickerFieldExtension,
RepoUrlPickerFieldExtension,
ScaffolderPage,
scaffolderPlugin as plugin,
Expand All @@ -42,6 +43,7 @@ export {
OwnerPicker,
RepoUrlPicker,
TextValuePicker,
OwnedEntityPicker,
} from './components/fields';
export { FavouriteTemplate } from './components/FavouriteTemplate';
export { TemplateList } from './components/TemplateList';
Expand Down
8 changes: 8 additions & 0 deletions plugins/scaffolder/src/plugin.ts
Expand Up @@ -35,6 +35,7 @@ import {
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker';

export const scaffolderPlugin = createPlugin({
id: 'scaffolder',
Expand Down Expand Up @@ -95,3 +96,10 @@ export const ScaffolderPage = scaffolderPlugin.provide(
mountPoint: rootRouteRef,
}),
);

export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
component: OwnedEntityPicker,
name: 'OwnedEntityPicker',
}),
);

0 comments on commit 0f645a7

Please sign in to comment.