feat: add GroupListPicker component

Signed-off-by: djamaile <rdjamaile@gmail.com>
This commit is contained in:
djamaile
2022-10-05 16:42:36 +02:00
parent 037b4393c4
commit 3e9e8203f3
4 changed files with 272 additions and 0 deletions
@@ -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<CatalogApi>;
const apis = TestApiRegistry.from([catalogApiRef, mockCatalogApi]);
describe('<GroupListPicker />', () => {
it('renders group list picker', () => {
const { queryByText } = render(
<ApiProvider apis={apis}>
<GroupListPicker
label="Search"
groupTypes={['org', 'department']}
defaultGroup="test"
/>
</ApiProvider>,
);
expect(queryByText('test')).toBeInTheDocument();
});
it('open group list picker', () => {
const { getByTestId, getAllByText } = render(
<ApiProvider apis={apis}>
<GroupListPicker
label="Search unique"
groupTypes={['org', 'department']}
/>
</ApiProvider>,
);
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(
<ApiProvider apis={apis}>
<GroupListPicker
label="Search unique"
groupTypes={['org', 'department']}
/>
</ApiProvider>,
);
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();
});
});
});
@@ -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<string>;
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<any, 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 <ResponseErrorPanel error={error} />;
}
return (
<>
<Popover
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
>
<Autocomplete
data-testid="group-list-picker-input"
area-aria-describedby={id}
loading={loading}
options={groups ?? []}
groupBy={option => 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 => (
<TextField {...params} label={label} variant="outlined" />
)}
/>
</Popover>
<button
id={id}
style={{ cursor: 'pointer' }}
onClick={handleClick}
className={classes.btn}
data-testid="group-list-picker-button"
>
<Box
display="flex"
flexDirection="row"
alignItems="center"
style={{ gap: '4px' }}
>
<PeopleIcon fontSize="large" />
<Typography variant="h3" className={classes.title}>
{group}
</Typography>
<KeyboardArrowDownIcon fontSize="large" />
</Box>
</button>
</>
);
};
@@ -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';
@@ -28,3 +28,4 @@ export * from './InspectEntityDialog';
export * from './UnregisterEntityDialog';
export * from './UserListPicker';
export * from './EntityProcessingStatusPicker';
export * from './GroupListPicker';