feat: reworking the picker slightly to just use groups and decorate the meta for now

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2023-06-13 13:15:47 +02:00
parent 0278da3d5c
commit 67d7412d4f
2 changed files with 146 additions and 55 deletions
@@ -22,7 +22,12 @@ import { OwnershipEntityRefPicker } from './OwnershipEntityRefPicker';
import { TestApiProvider } from '@backstage/test-utils';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
import {
ErrorApi,
IdentityApi,
errorApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import userEvent from '@testing-library/user-event';
// Create a mock IdentityApi
@@ -52,9 +57,14 @@ describe('<OwnershipEntityRefPicker />', () => {
const required = false;
const catalogApi: jest.Mocked<CatalogApi> = {
getEntities: jest.fn(async () => ({ items: entities })),
getEntitiesByRefs: jest.fn(async () => ({ items: entities })),
} as any;
const mockErrorApi: jest.Mocked<ErrorApi> = {
post: jest.fn(),
error$: jest.fn(),
};
beforeEach(() => {
entities = [
{
@@ -78,7 +88,7 @@ describe('<OwnershipEntityRefPicker />', () => {
];
onChange.mockClear();
catalogApi.getEntities.mockClear();
catalogApi.getEntitiesByRefs.mockClear();
});
afterEach(() => {
@@ -93,7 +103,7 @@ describe('<OwnershipEntityRefPicker />', () => {
entity.spec.members.includes('Bob'),
);
catalogApi.getEntities.mockResolvedValue({ items: userGroups });
catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups });
const props = {
onChange,
@@ -106,6 +116,7 @@ describe('<OwnershipEntityRefPicker />', () => {
apis={[
[identityApiRef, mockIdentityApi],
[catalogApiRef, catalogApi],
[errorApiRef, mockErrorApi],
]}
>
<OwnershipEntityRefPicker {...props} />
@@ -113,38 +124,35 @@ describe('<OwnershipEntityRefPicker />', () => {
);
await waitFor(() =>
expect(catalogApi.getEntities).toHaveBeenCalledTimes(1),
expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1),
);
expect(catalogApi.getEntities).toHaveBeenCalledWith({
filter: {
kind: ['Group'],
'relations.hasMember': ['group:default/group1', 'group:default/group2'],
},
expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledWith({
entityRefs: ['group:default/group1', 'group:default/group2'],
});
// Check that getEntities was set up to return the correct data
await expect(catalogApi.getEntities.mock.results[0].value).resolves.toEqual(
{
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group1' },
spec: { members: ['Bob'] },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group2' },
spec: { members: ['Bob'] },
},
],
},
);
await expect(
catalogApi.getEntitiesByRefs.mock.results[0].value,
).resolves.toEqual({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group1' },
spec: { members: ['Bob'] },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group2' },
spec: { members: ['Bob'] },
},
],
});
await expect(
catalogApi.getEntities.mock.results[0].value,
catalogApi.getEntitiesByRefs.mock.results[0].value,
).resolves.not.toEqual(
expect.objectContaining({
items: expect.arrayContaining([
@@ -163,7 +171,7 @@ describe('<OwnershipEntityRefPicker />', () => {
Array.isArray(entity.spec.members) &&
entity.spec.members.includes('Bob'),
);
catalogApi.getEntities.mockResolvedValue({ items: userGroups });
catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups });
const props = {
onChange,
@@ -176,6 +184,7 @@ describe('<OwnershipEntityRefPicker />', () => {
apis={[
[identityApiRef, mockIdentityApi],
[catalogApiRef, catalogApi],
[errorApiRef, mockErrorApi],
]}
>
<OwnershipEntityRefPicker {...props} />
@@ -183,7 +192,7 @@ describe('<OwnershipEntityRefPicker />', () => {
);
await waitFor(() =>
expect(catalogApi.getEntities).toHaveBeenCalledTimes(1),
expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1),
);
// Simulate user input
@@ -202,4 +211,62 @@ describe('<OwnershipEntityRefPicker />', () => {
// Assert that 'group3' is not rendered in the component
expect(queryByText('group3')).not.toBeInTheDocument();
});
it('should call the onChange handler with the correct entityRef and and use a nice display name', async () => {
const userGroups = [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group1', title: 'My First Group' },
spec: { members: ['Bob'] },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: { name: 'group2', title: 'My Second Group' },
spec: { members: ['Bob'] },
},
];
catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups });
const props = {
onChange,
schema,
required,
} as unknown as FieldProps<any>;
const { getByRole } = render(
<TestApiProvider
apis={[
[identityApiRef, mockIdentityApi],
[catalogApiRef, catalogApi],
[errorApiRef, mockErrorApi],
]}
>
<OwnershipEntityRefPicker {...props} />
</TestApiProvider>,
);
await waitFor(() =>
expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1),
);
const inputField = getByRole('combobox');
userEvent.click(inputField);
userEvent.type(inputField, 'group');
await waitFor(() => {
const option = getByRole('option', { name: 'My First Group' });
expect(option).toBeInTheDocument();
});
const option = getByRole('option', { name: 'My First Group' });
userEvent.click(option);
await waitFor(() => {
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('group:default/group1');
});
});
});
@@ -14,8 +14,12 @@
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
import React, { useState } from 'react';
import {
errorApiRef,
identityApiRef,
useApi,
} from '@backstage/core-plugin-api';
import { TextField, FormControl } from '@material-ui/core';
import {
OwnershipEntityRefPickerProps,
@@ -24,6 +28,8 @@ import {
import { Autocomplete } from '@material-ui/lab';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { NotFoundError } from '@backstage/errors';
import useAsync from 'react-use/lib/useAsync';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
export { OwnershipEntityRefPickerSchema };
@@ -34,34 +40,52 @@ export const OwnershipEntityRefPicker = (
schema: { title, description },
required,
rawErrors,
onChange,
} = props;
const identityApi = useApi(identityApiRef);
const catalogApi = useApi(catalogApiRef);
const [groups, setGroups] = useState<string[]>([]);
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
const errorApi = useApi(errorApiRef);
const [groups, setGroups] = useState<
{
label: string;
ref: string;
}[]
>([]);
const [selectedGroup, setSelectedGroup] = useState<null | {
label: string;
ref: string;
}>(null);
useEffect(() => {
const fetchUserGroups = async () => {
const identity = await identityApi.getBackstageIdentity();
const userIdentity = identity.ownershipEntityRefs;
useAsync(async () => {
const { ownershipEntityRefs } = await identityApi.getBackstageIdentity();
if (!userIdentity) {
throw new NotFoundError('No ownership entity refs found');
}
if (!ownershipEntityRefs || !ownershipEntityRefs.length) {
errorApi.post(new NotFoundError('No ownership entity refs found'));
return;
}
const userOwnedGroups = await catalogApi.getEntities({
filter: {
kind: ['Group'],
'relations.hasMember': userIdentity,
},
});
const groupValues = userOwnedGroups.items.map(item => item.metadata.name);
setGroups(groupValues);
};
const { items } = await catalogApi.getEntitiesByRefs({
entityRefs: ownershipEntityRefs,
});
fetchUserGroups();
}, [identityApi, catalogApi]);
const groupValues = items
.filter((e): e is Entity => Boolean(e))
.map(item => ({
label: item.metadata.title ?? item.metadata.name,
ref: stringifyEntityRef(item),
}));
setGroups(groupValues);
});
const updateChange = (
_: React.ChangeEvent<{}>,
value: { label: string; ref: string } | null,
) => {
setSelectedGroup(value);
onChange(value?.ref ?? '');
};
return (
<FormControl
@@ -73,8 +97,8 @@ export const OwnershipEntityRefPicker = (
id="OwnershipEntityRefPicker-dropdown"
options={groups || []}
value={selectedGroup}
onChange={(_, value) => setSelectedGroup(value || '')}
getOptionLabel={group => group}
onChange={updateChange}
getOptionLabel={group => group.label}
renderInput={params => (
<TextField
{...params}