diff --git a/plugins/catalog-react/src/components/GroupListPicker/GroupListPicker.test.tsx b/plugins/catalog-react/src/components/GroupListPicker/GroupListPicker.test.tsx new file mode 100644 index 0000000000..77d1eead6d --- /dev/null +++ b/plugins/catalog-react/src/components/GroupListPicker/GroupListPicker.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright 2022 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 React from 'react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { ApiProvider } from '@backstage/core-app-api'; +import { catalogApiRef } from '../../api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { GroupListPicker } from '../GroupListPicker'; +import { GroupEntity } from '@backstage/catalog-model'; +import { TestApiRegistry } from '@backstage/test-utils'; + +const mockGroups: GroupEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + namespace: 'default', + name: 'group-a', + }, + spec: { + type: 'org', + profile: { + displayName: 'Group A', + }, + children: [], + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + namespace: 'default', + name: 'group-b', + }, + spec: { + type: 'department', + profile: { + displayName: 'Group B', + }, + children: [], + }, + }, +]; + +const mockCatalogApi = { + getEntities: () => Promise.resolve({ items: mockGroups }), +} as Partial; + +const apis = TestApiRegistry.from([catalogApiRef, mockCatalogApi]); + +describe('', () => { + it('renders group list picker', () => { + const { queryByText } = render( + + + , + ); + + expect(queryByText('test')).toBeInTheDocument(); + }); + + it('open group list picker', () => { + const { getByTestId, getAllByText } = render( + + + , + ); + + fireEvent.click(getByTestId('group-list-picker-button')); + expect(getAllByText('Search unique').length).toBeGreaterThan(0); + }); + + it('can choose a group', async () => { + const { getByText, queryByText, getByTestId } = render( + + + , + ); + + fireEvent.click(getByTestId('group-list-picker-button')); + const input = getByTestId('group-list-picker-input').querySelector('input'); + fireEvent.change(input as HTMLElement, { target: { value: 'GR' } }); + + await waitFor(() => { + expect(queryByText('Group A')).toBeInTheDocument(); + fireEvent.click(getByText('Group A')); + expect(getByText('Group A')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/catalog-react/src/components/GroupListPicker/GroupListPicker.tsx b/plugins/catalog-react/src/components/GroupListPicker/GroupListPicker.tsx new file mode 100644 index 0000000000..ba0e2841af --- /dev/null +++ b/plugins/catalog-react/src/components/GroupListPicker/GroupListPicker.tsx @@ -0,0 +1,140 @@ +/* + * Copyright 2022 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 React from 'react'; +import { catalogApiRef } from '../../api'; +import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import useAsync from 'react-use/lib/useAsync'; +import Popover from '@material-ui/core/Popover'; +import { useApi } from '@backstage/core-plugin-api'; +import { ResponseErrorPanel } from '@backstage/core-components'; +import { GroupEntity } from '@backstage/catalog-model'; +import { makeStyles, Box, Typography } from '@material-ui/core'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import PeopleIcon from '@material-ui/icons/People'; + +const useStyles = makeStyles({ + btn: { + backgroundColor: 'transparent', + border: 'none', + margin: 0, + padding: 0, + }, + title: { + fontStyle: 'normal', + fontWeight: 700, + fontSize: '24px', + lineHeight: '32px', + letterSpacing: '-0.25px', + }, +}); + +type GroupListPickerProps = { + label: string; + groupTypes: Array; + defaultGroup?: string; +}; +export const GroupListPicker = (props: GroupListPickerProps) => { + const classes = useStyles(); + const catalogApi = useApi(catalogApiRef); + const { label, groupTypes, defaultGroup = '' } = props; + const [anchorEl, setAnchorEl] = React.useState(null); + const [inputValue, setInputValue] = React.useState(''); + const [group, setGroup] = React.useState(defaultGroup); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + const id = open ? 'simple-popover' : undefined; + + const { + loading, + error, + value: groups, + } = useAsync(async () => { + const groupsList = await catalogApi.getEntities({ + filter: { + kind: 'Group', + 'spec.type': groupTypes, + }, + }); + + return groupsList.items as GroupEntity[]; + }, [catalogApi]); + + if (error) { + return ; + } + + return ( + <> + + option.spec.type} + getOptionLabel={option => option.spec.profile?.displayName ?? ''} + inputValue={inputValue} + onInputChange={(_, value) => setInputValue(value)} + onChange={(_, newValue) => { + if (newValue) { + setGroup(newValue.spec.profile?.displayName ?? ''); + } + setInputValue(''); + }} + style={{ width: '200px', margin: '8px' }} + renderInput={params => ( + + )} + /> + + + + ); +}; diff --git a/plugins/catalog-react/src/components/GroupListPicker/index.ts b/plugins/catalog-react/src/components/GroupListPicker/index.ts new file mode 100644 index 0000000000..be2819c850 --- /dev/null +++ b/plugins/catalog-react/src/components/GroupListPicker/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { GroupListPicker } from './GroupListPicker'; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index c604306ead..90fe108ab7 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -28,3 +28,4 @@ export * from './InspectEntityDialog'; export * from './UnregisterEntityDialog'; export * from './UserListPicker'; export * from './EntityProcessingStatusPicker'; +export * from './GroupListPicker';