diff --git a/packages/core/src/icons/types.ts b/packages/core/src/icons/types.ts index 30e0ba53b2..599cb969df 100644 --- a/packages/core/src/icons/types.ts +++ b/packages/core/src/icons/types.ts @@ -16,7 +16,6 @@ import { ComponentType } from 'react'; import { SvgIconProps } from '@material-ui/core'; - export type IconComponent = ComponentType; export type SystemIconKey = 'user' | 'group'; export type SystemIcons = { [key in SystemIconKey]: IconComponent }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 013c80e2fa..816ad92578 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -46,3 +46,4 @@ export { default as TrendLine } from './components/TrendLine'; export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular'; export * from './components/Status'; export { default as WarningPanel } from './components/WarningPanel'; +export type { IconComponent } from './icons'; diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx new file mode 100644 index 0000000000..762a881302 --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -0,0 +1,131 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { render, fireEvent } from '@testing-library/react'; +import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; + +describe('Catalog Filter', () => { + it('should render the different groups', async () => { + const mockGroups: CatalogFilterGroup[] = [ + { name: 'Test Group 1', items: [] }, + { name: 'Test Group 2', items: [] }, + ]; + const { findByText } = render( + wrapInThemedTestApp(), + ); + + for (const group of mockGroups) { + expect(await findByText(group.name)).toBeInTheDocument(); + } + }); + + it('should render the different items and their names', async () => { + const mockGroups: CatalogFilterGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'first', + label: 'First Label', + }, + { + id: 'second', + label: 'Second Label', + }, + ], + }, + ]; + + const { findByText } = render( + wrapInThemedTestApp(), + ); + + const [group] = mockGroups; + for (const item of group.items) { + expect(await findByText(item.label)).toBeInTheDocument(); + } + }); + + it('should render the count in each item', async () => { + const mockGroups: CatalogFilterGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'first', + label: 'First Label', + count: 100, + }, + { + id: 'second', + label: 'Second Label', + count: 400, + }, + ], + }, + ]; + + const { findByText } = render( + wrapInThemedTestApp(), + ); + + const [group] = mockGroups; + for (const item of group.items) { + expect(await findByText(item.count!.toString())).toBeInTheDocument(); + } + }); + + it('should fire the callback when an item is clicked', async () => { + const mockGroups: CatalogFilterGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'first', + label: 'First Label', + count: 100, + }, + { + id: 'second', + label: 'Second Label', + count: 400, + }, + ], + }, + ]; + + const onSelectedChangeHandler = jest.fn(); + + const { findByText } = render( + wrapInThemedTestApp( + , + ), + ); + + const item = mockGroups[0].items[0]; + + const element = await findByText(item.label); + + fireEvent.click(element); + + expect(onSelectedChangeHandler).toHaveBeenCalledWith(item); + }); +}); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx new file mode 100644 index 0000000000..eb45d8a80b --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + Card, + List, + ListItemIcon, + ListItemText, + MenuItem, + Typography, + Theme, + makeStyles, +} from '@material-ui/core'; +import type { IconComponent } from '@backstage/core'; + +export type CatalogFilterItem = { + id: string; + label: string; + icon?: IconComponent; + count?: number; + loading?: boolean; +}; + +export type CatalogFilterGroup = { + name: string; + items: CatalogFilterItem[]; +}; + +export type CatalogFilterProps = { + groups: CatalogFilterGroup[]; + selectedId?: string; + onSelectedChange?: (item: CatalogFilterItem) => void; +}; + +const useStyles = makeStyles(theme => ({ + root: { + backgroundColor: 'rgba(0, 0, 0, .11)', + boxShadow: 'none', + }, + title: { + margin: theme.spacing(1, 0, 0, 1), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + }, + listIcon: { + minWidth: 30, + color: theme.palette.text.primary, + }, + menuItem: { + minHeight: theme.spacing(6), + }, + groupWrapper: { + margin: theme.spacing(1, 1, 2, 1), + }, + menuTitle: { + fontWeight: 500, + }, +})); + +export const CatalogFilter: React.FC = ({ + groups, + selectedId, + onSelectedChange, +}) => { + const classes = useStyles(); + return ( + + {groups.map(group => ( + + + {group.name} + + + + {group.items.map(item => ( + onSelectedChange?.(item)} + selected={item.id === selectedId} + className={classes.menuItem} + > + {item.icon && ( + + + + )} + + + {item.label} + + + {item.count} + + ))} + + + + ))} + + ); +}; diff --git a/plugins/catalog/src/components/CatalogFilter/index.ts b/plugins/catalog/src/components/CatalogFilter/index.ts new file mode 100644 index 0000000000..5103b16307 --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { CatalogFilter } from './CatalogFilter'; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 60af5f7d24..5dee366696 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -17,8 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import CatalogPage from './CatalogPage'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; +import { wrapInThemedTestApp } from '@backstage/test-utils'; import { ComponentFactory } from '../../data/component'; const testComponentFactory: ComponentFactory = { @@ -28,11 +27,14 @@ const testComponentFactory: ComponentFactory = { }; describe('CatalogPage', () => { + // this test right now causes some red lines in the log output when running tests + // related to some theme issues in mui-table + // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { const rendered = render( - - - , + wrapInThemedTestApp( + , + ), ); expect( await rendered.findByText('Keep track of your software'), diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ec6c1cf09f..f5094542a5 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -27,13 +27,38 @@ import { import { useAsync } from 'react-use'; import { ComponentFactory } from '../../data/component'; import CatalogTable from '../CatalogTable/CatalogTable'; -import { Button } from '@material-ui/core'; +import { + CatalogFilter, + CatalogFilterItem, +} from '../CatalogFilter/CatalogFilter'; +import { Button, makeStyles } from '@material-ui/core'; +import { filterGroups, defaultFilter } from '../../data/filters'; + +const useStyles = makeStyles(theme => ({ + contentWrapper: { + display: 'grid', + gridTemplateAreas: "'filters' 'table'", + gridTemplateColumns: '250px 1fr', + gridColumnGap: theme.spacing(2), + }, +})); type CatalogPageProps = { componentFactory: ComponentFactory; }; + const CatalogPage: FC = ({ componentFactory }) => { const { value, error, loading } = useAsync(componentFactory.getAllComponents); + const [selectedFilter, setSelectedFilter] = React.useState( + defaultFilter, + ); + + const onFilterSelected = React.useCallback( + selected => setSelectedFilter(selected), + [], + ); + + const styles = useStyles(); return (
@@ -46,11 +71,21 @@ const CatalogPage: FC = ({ componentFactory }) => { All your components - +
+
+ +
+ +
); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 99debb12d4..9bb1db4519 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -28,7 +28,9 @@ const components: Component[] = [ describe('CatalogTable component', () => { it('should render loading when loading prop it set to true', async () => { const rendered = render( - wrapInThemedTestApp(), + wrapInThemedTestApp( + , + ), ); const progress = await rendered.findByTestId('progress'); expect(progress).toBeInTheDocument(); @@ -38,6 +40,7 @@ describe('CatalogTable component', () => { const rendered = render( wrapInThemedTestApp( { it('should display component names when loading has finished and no error occurred', async () => { const rendered = render( wrapInThemedTestApp( - , + , ), ); + expect( + await rendered.findByText(`Owned (${components.length})`), + ).toBeInTheDocument(); expect(await rendered.findByText('component1')).toBeInTheDocument(); expect(await rendered.findByText('component2')).toBeInTheDocument(); expect(await rendered.findByText('component3')).toBeInTheDocument(); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 57f1dfe7f5..537deb1b30 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -63,6 +63,7 @@ const columns: TableColumn[] = [ type CatalogTableProps = { components: Component[]; + titlePreamble: string; loading: boolean; error?: any; }; @@ -70,6 +71,7 @@ const CatalogTable: FC = ({ components, loading, error, + titlePreamble, }) => { if (loading) { return ; @@ -87,7 +89,7 @@ const CatalogTable: FC = ({ ); diff --git a/plugins/catalog/src/data/filters.ts b/plugins/catalog/src/data/filters.ts new file mode 100644 index 0000000000..66eb55b478 --- /dev/null +++ b/plugins/catalog/src/data/filters.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + CatalogFilterGroup, + CatalogFilterItem, +} from '../components/CatalogFilter/CatalogFilter'; +import SettingsIcon from '@material-ui/icons/Settings'; +import StarIcon from '@material-ui/icons/Star'; + +export const filterGroups: CatalogFilterGroup[] = [ + { + name: 'Personal', + items: [ + { + id: 'owned', + label: 'Owned', + count: 123, + icon: SettingsIcon, + }, + { + id: 'starred', + label: 'Starred', + count: 10, + icon: StarIcon, + }, + ], + }, + { + name: 'Spotify', + items: [ + { + id: 'all', + label: 'All Services', + count: 123, + }, + ], + }, +]; + +export const defaultFilter: CatalogFilterItem = filterGroups[0].items[0];