diff --git a/.changeset/great-planes-arrive.md b/.changeset/great-planes-arrive.md new file mode 100644 index 0000000000..563c061627 --- /dev/null +++ b/.changeset/great-planes-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org-react': minor +--- + +Implemented the org-react plugin, with it's first component being: a `GroupListPicker` component that will give the user the ability to choose a group diff --git a/plugins/org-react/.eslintrc.js b/plugins/org-react/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/org-react/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/org-react/README.md b/plugins/org-react/README.md new file mode 100644 index 0000000000..91c7ebf48a --- /dev/null +++ b/plugins/org-react/README.md @@ -0,0 +1,30 @@ +# org-react + +## features + +- Group list picker component + +### GroupListPicker + +The `GroupListPicker` component displays a select box which also has autocomplete functionality. + +To use the `GroupListPicker` component you'll need to import it and add it to your desired place. + +```diff ++ import { GroupListPicker } from '@backstage/plugin-org-react'; ++ import React, { useState } from 'react'; + ++ const [group, setGroup] = useState(); + + + ++ + + +``` + +The `GroupListPicker` comes with three props: + +- `groupTypes`: gives the user the option which group types the component should load. If no value is provided all group types will be loaded in; +- `placeholder`: the placeholder that the select box in the component should display. This might be helpful in informing your users what the functionality of the component is. +- `onChange`: a prop to help the user to give access to the selected group diff --git a/plugins/org-react/api-report.md b/plugins/org-react/api-report.md new file mode 100644 index 0000000000..60cd6944d0 --- /dev/null +++ b/plugins/org-react/api-report.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/plugin-org-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { GroupEntity } from '@backstage/catalog-model'; + +// @public (undocumented) +export const GroupListPicker: (props: GroupListPickerProps) => JSX.Element; + +// @public +export type GroupListPickerProps = { + placeholder?: string; + groupTypes?: Array; + onChange: (value: GroupEntity | undefined) => void; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json new file mode 100644 index 0000000000..b7b87f9aca --- /dev/null +++ b/plugins/org-react/package.json @@ -0,0 +1,62 @@ +{ + "name": "@backstage/plugin-org-react", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/org-react" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.47.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/org-react/src/components/GroupListPicker/GroupListPicker.test.tsx b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.test.tsx new file mode 100644 index 0000000000..b8a499b8fe --- /dev/null +++ b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.test.tsx @@ -0,0 +1,87 @@ +/* + * 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 '@backstage/plugin-catalog-react'; +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('can choose a group', async () => { + const { getByText, 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(async () => { + expect(getByText('Group A')).toBeInTheDocument(); + fireEvent.click(getByText('Group A')); + expect(getByText('Group A')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx new file mode 100644 index 0000000000..1296c520c3 --- /dev/null +++ b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx @@ -0,0 +1,120 @@ +/* + * 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, { useCallback } from 'react'; +import { + catalogApiRef, + humanizeEntityRef, +} from '@backstage/plugin-catalog-react'; +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 { Entity, GroupEntity } from '@backstage/catalog-model'; +import { GroupListPickerButton } from './GroupListPickerButton'; + +/** + * Props for {@link GroupListPicker}. + * + * @public + */ +export type GroupListPickerProps = { + placeholder?: string; + groupTypes?: Array; + onChange: (value: GroupEntity | undefined) => void; +}; + +/** @public */ +export const GroupListPicker = (props: GroupListPickerProps) => { + const catalogApi = useApi(catalogApiRef); + + const { onChange, groupTypes, placeholder = '' } = props; + const [anchorEl, setAnchorEl] = React.useState(null); + const [inputValue, setInputValue] = React.useState(''); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + + const { + loading, + error, + value: groups, + } = useAsync(async () => { + const groupsList = await catalogApi.getEntities({ + filter: { + kind: 'Group', + 'spec.type': groupTypes || [], + }, + }); + + return groupsList.items as GroupEntity[]; + }, [catalogApi, groupTypes]); + + const handleChange = useCallback( + (_, v: GroupEntity | null) => { + onChange(v ?? undefined); + }, + [onChange], + ); + + if (error) { + return ; + } + + const getHumanEntityRef = (entity: Entity) => humanizeEntityRef(entity); + + return ( + <> + + option.spec.type} + getOptionLabel={option => + option.spec.profile?.displayName ?? getHumanEntityRef(option) + } + inputValue={inputValue} + onInputChange={(_, value) => setInputValue(value)} + onChange={handleChange} + style={{ width: '300px' }} + renderInput={params => ( + + )} + /> + + + + ); +}; diff --git a/plugins/org-react/src/components/GroupListPicker/GroupListPickerButton.tsx b/plugins/org-react/src/components/GroupListPicker/GroupListPickerButton.tsx new file mode 100644 index 0000000000..149bf778b1 --- /dev/null +++ b/plugins/org-react/src/components/GroupListPicker/GroupListPickerButton.tsx @@ -0,0 +1,68 @@ +/* + * 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 { BackstageTheme } from '@backstage/theme'; +import { makeStyles, Typography, Button } from '@material-ui/core'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import PeopleIcon from '@material-ui/icons/People'; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + btn: { + margin: 0, + padding: 10, + width: '100%', + cursor: 'pointer', + justifyContent: 'space-between', + }, + title: { + fontSize: '1.5rem', + fontStyle: 'normal', + fontWeight: theme.typography.fontWeightBold, + letterSpacing: '-0.25px', + lineHeight: '32px', + marginBottom: 0, + textTransform: 'none', + }, + icon: { + transform: 'scale(1.5)', + }, +})); + +type GroupListPickerButtonProps = { + handleClick: (event: React.MouseEvent) => void; + group: string | undefined; +}; + +/** @public */ +export const GroupListPickerButton = (props: GroupListPickerButtonProps) => { + const { handleClick, group } = props; + const classes = useStyles(); + + return ( + + ); +}; diff --git a/plugins/org-react/src/components/GroupListPicker/index.ts b/plugins/org-react/src/components/GroupListPicker/index.ts new file mode 100644 index 0000000000..fe1ba8e1ee --- /dev/null +++ b/plugins/org-react/src/components/GroupListPicker/index.ts @@ -0,0 +1,18 @@ +/* + * 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'; +export type { GroupListPickerProps } from './GroupListPicker'; diff --git a/plugins/org-react/src/index.ts b/plugins/org-react/src/index.ts new file mode 100644 index 0000000000..c3393bee48 --- /dev/null +++ b/plugins/org-react/src/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 './components/GroupListPicker'; +export type { GroupListPickerProps } from './components/GroupListPicker'; diff --git a/plugins/org-react/src/setupTests.ts b/plugins/org-react/src/setupTests.ts new file mode 100644 index 0000000000..9bb3e72355 --- /dev/null +++ b/plugins/org-react/src/setupTests.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. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index 7f530482c2..71494a00f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6372,6 +6372,35 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-org-react@workspace:plugins/org-react": + version: 0.0.0-use.local + resolution: "@backstage/plugin-org-react@workspace:plugins/org-react" + dependencies: + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + msw: ^0.47.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-org@workspace:^, @backstage/plugin-org@workspace:plugins/org": version: 0.0.0-use.local resolution: "@backstage/plugin-org@workspace:plugins/org"