diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index c02aca5659..047a6b1990 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -14,18 +14,8 @@ * limitations under the License. */ -import React, { FC, forwardRef } from 'react'; -import MTable, { - MTableCell, - MTableHeader, - MTableToolbar, - MaterialTableProps, - Options, - Column, -} from 'material-table'; import { BackstageTheme } from '@backstage/theme'; -import { makeStyles, useTheme, Typography } from '@material-ui/core'; - +import { makeStyles, Typography, useTheme } from '@material-ui/core'; // Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51 import AddBox from '@material-ui/icons/AddBox'; import ArrowUpward from '@material-ui/icons/ArrowUpward'; @@ -42,6 +32,15 @@ import Remove from '@material-ui/icons/Remove'; import SaveAlt from '@material-ui/icons/SaveAlt'; import Search from '@material-ui/icons/Search'; import ViewColumn from '@material-ui/icons/ViewColumn'; +import MTable, { + Column, + MaterialTableProps, + MTableCell, + MTableHeader, + MTableToolbar, + Options, +} from 'material-table'; +import React, { forwardRef } from 'react'; const tableIcons = { Add: forwardRef((props, ref: React.Ref) => ( @@ -131,10 +130,10 @@ const useToolbarStyles = makeStyles(theme => ({ }, })); -const convertColumns = ( - columns: TableColumn[], +function convertColumns( + columns: TableColumn[], theme: BackstageTheme, -): TableColumn[] => { +): TableColumn[] { return columns.map(column => { const headerStyle: React.CSSProperties = {}; const cellStyle: React.CSSProperties = {}; @@ -150,25 +149,26 @@ const convertColumns = ( cellStyle, }; }); -}; +} -export interface TableColumn extends Column<{}> { +export interface TableColumn extends Column { highlight?: boolean; width?: string; } -export interface TableProps extends MaterialTableProps<{}> { - columns: TableColumn[]; +export interface TableProps + extends MaterialTableProps { + columns: TableColumn[]; subtitle?: string; } -export const Table: FC = ({ +export function Table({ columns, options, title, subtitle, ...props -}) => { +}: TableProps) { const cellClasses = useCellStyles(); const headerClasses = useHeaderStyles(); const toolbarClasses = useToolbarStyles(); @@ -183,7 +183,7 @@ export const Table: FC = ({ }; return ( - components={{ Cell: cellProps => ( @@ -211,4 +211,4 @@ export const Table: FC = ({ {...props} /> ); -}; +} diff --git a/packages/core/src/components/Table/index.ts b/packages/core/src/components/Table/index.ts index aeade2b24f..c10399dc59 100644 --- a/packages/core/src/components/Table/index.ts +++ b/packages/core/src/components/Table/index.ts @@ -15,5 +15,5 @@ */ export { Table } from './Table'; -export type { TableColumn } from './Table'; +export type { TableColumn, TableProps } from './Table'; export { SubvalueCell } from './SubvalueCell'; diff --git a/packages/core/src/layout/HeaderTabs/index.tsx b/packages/core/src/layout/HeaderTabs/index.tsx index 2617a27aa9..838b126569 100644 --- a/packages/core/src/layout/HeaderTabs/index.tsx +++ b/packages/core/src/layout/HeaderTabs/index.tsx @@ -42,14 +42,15 @@ export type Tab = { id: string; label: string; }; + export const HeaderTabs: React.FC<{ tabs: Tab[]; - onChange?: (index: Number) => void; + onChange?: (index: number) => void; }> = ({ tabs, onChange }) => { - const [selectedTab, setSelectedTab] = useState(0); + const [selectedTab, setSelectedTab] = useState(0); const styles = useStyles(); - const handleChange = (_: React.ChangeEvent<{}>, index: Number) => { + const handleChange = (_: React.ChangeEvent<{}>, index: number) => { setSelectedTab(index); if (onChange) onChange(index); }; diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index 9dd0caa3b4..a98eec0875 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -26,9 +26,8 @@ import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; import { fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { CatalogApi, catalogApiRef } from '../../api/types'; -import { EntityGroup } from '../../data/filters'; import { EntityFilterGroupsProvider } from '../../filter'; -import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; +import { ButtonGroup, CatalogFilter } from './CatalogFilter'; describe('Catalog Filter', () => { const catalogApi: Partial = { @@ -79,12 +78,12 @@ describe('Catalog Filter', () => { ); it('should render the different groups', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [] }, { name: 'Test Group 2', items: [] }, ]; const { findByText } = renderWrapped( - , + , ); for (const group of mockGroups) { expect(await findByText(group.name)).toBeInTheDocument(); @@ -92,24 +91,26 @@ describe('Catalog Filter', () => { }); it('should render the different items and their names', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [ { - id: EntityGroup.ALL, + id: 'all', label: 'First Label', + filterFn: () => true, }, { - id: EntityGroup.STARRED, + id: 'starred', label: 'Second Label', + filterFn: () => false, }, ], }, ]; const { findByText } = renderWrapped( - , + , ); for (const item of mockGroups[0].items) { @@ -118,48 +119,19 @@ describe('Catalog Filter', () => { }); it('selects the first item if no desired initial one is set', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [ { - id: EntityGroup.ALL, + id: 'all', label: 'First Label', + filterFn: () => true, }, { - id: EntityGroup.STARRED, - label: 'Second Label', - }, - ], - }, - ]; - - const onChange = jest.fn(); - - renderWrapped( - , - ); - - await waitFor(() => { - expect(onChange).toHaveBeenLastCalledWith({ - id: EntityGroup.ALL, - label: 'First Label', - }); - }); - }); - - it('selects the initial item', async () => { - const mockGroups: CatalogFilterGroup[] = [ - { - name: 'Test Group 1', - items: [ - { - id: EntityGroup.ALL, - label: 'First Label', - }, - { - id: EntityGroup.STARRED, + id: 'starred', label: 'Second Label', + filterFn: () => false, }, ], }, @@ -169,32 +141,71 @@ describe('Catalog Filter', () => { renderWrapped( , ); await waitFor(() => { expect(onChange).toHaveBeenLastCalledWith({ - id: EntityGroup.STARRED, + id: 'all', + label: 'First Label', + }); + }); + }); + + it('selects the initial item', async () => { + const mockGroups: ButtonGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'all', + label: 'First Label', + filterFn: () => true, + }, + { + id: 'starred', + label: 'Second Label', + filterFn: () => false, + }, + ], + }, + ]; + + const onChange = jest.fn(); + + renderWrapped( + , + ); + + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith({ + id: 'starred', label: 'Second Label', }); }); }); it('can change the selected item', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [ { - id: EntityGroup.ALL, + id: 'all', label: 'First Label', + filterFn: () => true, }, { - id: EntityGroup.STARRED, + id: 'starred', label: 'Second Label', + filterFn: () => false, }, ], }, @@ -203,12 +214,16 @@ describe('Catalog Filter', () => { const onChange = jest.fn(); const { findByText } = renderWrapped( - , + , ); await waitFor(() => { expect(onChange).toHaveBeenLastCalledWith({ - id: EntityGroup.ALL, + id: 'all', label: 'First Label', }); }); @@ -217,27 +232,28 @@ describe('Catalog Filter', () => { await waitFor(() => { expect(onChange).toHaveBeenLastCalledWith({ - id: EntityGroup.STARRED, + id: 'starred', label: 'Second Label', }); }); }); it('displays match counts properly', async () => { - const mockGroups: CatalogFilterGroup[] = [ + const mockGroups: ButtonGroup[] = [ { name: 'Test Group 1', items: [ { - id: EntityGroup.OWNED, + id: 'owned', label: 'First Label', + filterFn: entity => entity.spec?.owner === 'tools@example.com', }, ], }, ]; const { findByText } = renderWrapped( - , + , ); expect(await findByText('1')).toBeInTheDocument(); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index 71956e2e22..90a1e15ad0 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { IconComponent, identityApiRef, useApi } from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { IconComponent } from '@backstage/core'; import { Card, List, @@ -26,25 +27,23 @@ import { Theme, Typography, } from '@material-ui/core'; -import React, { FC, useCallback, useMemo, useState, useEffect } from 'react'; -import { - EntityFilterOptions, - entityFilters, - EntityGroup, -} from '../../data/filters'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { FilterGroup, useEntityFilterGroup } from '../../filter'; -import { useStarredEntities } from '../../hooks/useStarredEntites'; -export type CatalogFilterItem = { - id: EntityGroup; - label: string; - icon?: IconComponent; - count?: number | FC; -}; - -export type CatalogFilterGroup = { +export type ButtonGroup = { name: string; - items: CatalogFilterItem[]; + items: { + id: string; + label: string; + icon?: IconComponent; + filterFn: (entity: Entity) => boolean; + }[]; }; const useStyles = makeStyles(theme => ({ @@ -73,35 +72,48 @@ const useStyles = makeStyles(theme => ({ }, })); +type OnChangeCallback = (item: { id: string; label: string }) => void; + type Props = { - filterGroups: CatalogFilterGroup[]; - onChange?: (filterItem: CatalogFilterItem) => void; - initiallySelected?: EntityGroup; + buttonGroups: ButtonGroup[]; + initiallySelected: string; + onChange?: OnChangeCallback; }; +/** + * The main filter group in the sidebar, toggling owned/starred/all. + */ export const CatalogFilter = ({ - filterGroups, + buttonGroups, onChange, initiallySelected, }: Props) => { const classes = useStyles(); - const { currentFilter, setCurrentFilter, getFilterCount } = useFilter(); + const { currentFilter, setCurrentFilter, getFilterCount } = useFilter( + buttonGroups, + initiallySelected, + ); + + const onChangeRef = useRef(); + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); const setCurrent = useCallback( - (item: CatalogFilterItem) => { + (item: { id: string; label: string }) => { setCurrentFilter(item.id); - onChange?.(item); + onChangeRef.current?.({ id: item.id, label: item.label }); }, - [onChange, setCurrentFilter], + [setCurrentFilter], ); // Make one initial onChange to inform the surroundings about the selected // item useEffect(() => { - const items = filterGroups.flatMap(g => g.items); + const items = buttonGroups.flatMap(g => g.items); const item = items.find(i => i.id === initiallySelected) || items[0]; if (item) { - onChange?.(item); + onChangeRef.current?.({ id: item.id, label: item.label }); } // intentionally only happens on startup // eslint-disable-next-line react-hooks/exhaustive-deps @@ -109,7 +121,7 @@ export const CatalogFilter = ({ return ( - {filterGroups.map(group => ( + {buttonGroups.map(group => ( {group.name} @@ -148,31 +160,29 @@ export const CatalogFilter = ({ ); }; -function useFilter(): { +function useFilter( + buttonGroups: ButtonGroup[], + initiallySelected: string, +): { currentFilter: string; setCurrentFilter: (filterId: string) => void; getFilterCount: (filterId: string) => number | undefined; } { - const [currentFilter, setCurrentFilter] = useState('OWNED'); - const { isStarredEntity } = useStarredEntities(); - const userId = useApi(identityApiRef).getUserId(); + const [currentFilter, setCurrentFilter] = useState(initiallySelected); - const filterGroup = useMemo(() => { - const result: FilterGroup = { filters: {} }; - const options: EntityFilterOptions = { - userId, - isStarred: isStarredEntity, - }; - for (const [filterId, filterFn] of Object.entries(entityFilters)) { - result.filters[filterId] = entity => filterFn(entity, options); - } - return result; - }, [isStarredEntity, userId]); + const filterGroup = useMemo( + () => ({ + filters: Object.fromEntries( + buttonGroups.flatMap(g => g.items).map(i => [i.id, i.filterFn]), + ), + }), + [buttonGroups], + ); const { setSelectedFilters, state } = useEntityFilterGroup( 'primary-sidebar', filterGroup, - ['OWNED'], + [initiallySelected], ); const setCurrent = useCallback( diff --git a/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx b/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx index 65715e04c7..39b3c66013 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx @@ -14,26 +14,29 @@ * limitations under the License. */ -import React, { FC } from 'react'; import { Header, HomepageTimer, + identityApiRef, Page, pageTheme, - identityApiRef, useApi, } from '@backstage/core'; +import React from 'react'; import { getTimeBasedGreeting } from './utils/timeUtil'; -const CatalogLayout: FC<{}> = props => { - const { children } = props; +type Props = { + children?: React.ReactNode; +}; + +const CatalogLayout = ({ children }: Props) => { const greeting = getTimeBasedGreeting(); - const identityApi = useApi(identityApiRef); + const userId = useApi(identityApiRef).getUserId(); return (
({ @@ -52,71 +46,77 @@ const useStyles = makeStyles(theme => ({ const CatalogPageContents = () => { const styles = useStyles(); - const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); const { loading, error, matchingEntities } = useFilteredEntities(); + const { isStarredEntity } = useStarredEntities(); + const userId = useApi(identityApiRef).getUserId(); const [selectedTab, setSelectedTab] = useState(); const [selectedSidebarItem, setSelectedSidebarItem] = useState(); - const YellowStar = withStyles({ - root: { - color: '#f3ba37', - }, - })(Star); + const tabs = useMemo( + () => [ + { + id: 'service', + label: 'Services', + }, + { + id: 'website', + label: 'Websites', + }, + { + id: 'library', + label: 'Libraries', + }, + { + id: 'documentation', + label: 'Documentation', + }, + { + id: 'other', + label: 'Other', + }, + ], + [], + ); - const actions = [ - (rowData: Entity) => { - const location = findLocationForEntityMeta(rowData.metadata); - return { - icon: GitHub, - tooltip: 'View on GitHub', - onClick: () => { - if (!location) return; - window.open(location.target, '_blank'); - }, - hidden: location?.type !== 'github', - }; - }, - (rowData: Entity) => { - const createEditLink = (location: LocationSpec): string => { - switch (location.type) { - case 'github': - return location.target.replace('/blob/', '/edit/'); - default: - return location.target; - } - }; - const location = findLocationForEntityMeta(rowData.metadata); - return { - icon: Edit, - tooltip: 'Edit', - iconProps: { size: 'small' }, - onClick: () => { - if (!location) return; - window.open(createEditLink(location), '_blank'); - }, - hidden: location?.type !== 'github', - }; - }, - (rowData: Entity) => { - const isStarred = isStarredEntity(rowData); - return { - icon: isStarred ? YellowStar : StarOutline, - tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites', - onClick: () => toggleStarredEntity(rowData), - }; - }, - ]; - - const onTabChanged = useCallback((type: LabeledEntityType) => { - setSelectedTab(type.label); - }, []); - const onSidebarChanged = useCallback((filterItem: CatalogFilterItem) => { - setSelectedSidebarItem(filterItem.label); - }, []); + const filterGroups = useMemo( + () => [ + { + name: 'Personal', + items: [ + { + id: 'owned', + label: 'Owned', + icon: SettingsIcon, + filterFn: entity => entity.spec?.owner === userId, + }, + { + id: 'starred', + label: 'Starred', + icon: StarIcon, + filterFn: isStarredEntity, + }, + ], + }, + { + name: 'Company', // TODO: Replace with Company name, read from app config. + items: [ + { + id: 'all', + label: 'All', + filterFn: () => true, + }, + ], + }, + ], + [isStarredEntity, userId], + ); return ( - + setSelectedTab(label)} + /> @@ -133,9 +133,9 @@ const CatalogPageContents = () => {
setSelectedSidebarItem(label)} + initiallySelected="owned" />
{ entities={matchingEntities} loading={loading} error={error} - actions={actions} />
diff --git a/plugins/catalog/src/components/CatalogPage/CatalogTabs.tsx b/plugins/catalog/src/components/CatalogPage/CatalogTabs.tsx index 1845404935..ae1e2a4e5a 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogTabs.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogTabs.tsx @@ -14,42 +14,72 @@ * limitations under the License. */ -import React, { useCallback, useEffect } from 'react'; -import { HeaderTabs } from '@backstage/core'; -import { labeledEntityTypes, LabeledEntityType } from '../../data/filters'; -import { useEntityFilterGroup, FilterGroup } from '../../filter'; import { Entity } from '@backstage/catalog-model'; +import { HeaderTabs } from '@backstage/core'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { FilterGroup, useEntityFilterGroup } from '../../filter'; -const filterGroup: FilterGroup = { - filters: Object.fromEntries( - labeledEntityTypes.map(t => [ - t.id, - (entity: Entity) => entity.spec?.type === t.id, - ]), - ), +/** + * A component type, and a human readable label for it. + */ +export type LabeledComponentType = { + id: string; + label: string; }; +/** + * Called on mount, and when the selected tab changes. + */ +export type OnChangeCallback = (tab: LabeledComponentType) => void; + type Props = { - onChange?: (type: LabeledEntityType) => void; + tabs: LabeledComponentType[]; + onChange?: OnChangeCallback; }; -export const CatalogTabs = ({ onChange }: Props) => { +/** + * The tabs at the top of the catalog list page, for component type filtering. + */ +export const CatalogTabs = ({ tabs, onChange }: Props) => { + const filterGroup = useMemo(() => { + return { + filters: Object.fromEntries( + tabs.map(t => [t.id, (entity: Entity) => entity.spec?.type === t.id]), + ), + }; + }, [tabs]); + const { setSelectedFilters } = useEntityFilterGroup('type', filterGroup, [ - labeledEntityTypes[0].id, + tabs[0].id, ]); - const onChangeFn = useCallback( - (index: Number) => { - const type = labeledEntityTypes[index as number]; - setSelectedFilters([type.id]); - onChange?.(type); - }, - [onChange, setSelectedFilters], - ); + const [currentTabIndex, setCurrentTabIndex] = useState(0); + // Hold a reference to the callback + const onChangeRef = useRef(); useEffect(() => { - onChange?.(labeledEntityTypes[0]); + onChangeRef.current = onChange; }, [onChange]); - return ; + useEffect(() => { + onChangeRef.current?.(tabs[currentTabIndex]); + }, [tabs, currentTabIndex]); + + const switchTab = useCallback( + (index: number) => { + const tab = tabs[index]; + setSelectedFilters([tab.id]); + setCurrentTabIndex(index); + onChangeRef.current?.(tab); + }, + [tabs, setSelectedFilters], + ); + + return ; }; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index b50da60a50..707213f3bb 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -66,11 +66,9 @@ describe('CatalogTable component', () => { />, ), ); - expect( - await rendered.findByText(`Owned (${entites.length})`), - ).toBeInTheDocument(); - expect(await rendered.findByText('component1')).toBeInTheDocument(); - expect(await rendered.findByText('component2')).toBeInTheDocument(); - expect(await rendered.findByText('component3')).toBeInTheDocument(); + expect(rendered.getByText(/Owned \(3\)/)).toBeInTheDocument(); + expect(rendered.getByText(/component1/)).toBeInTheDocument(); + expect(rendered.getByText(/component2/)).toBeInTheDocument(); + expect(rendered.getByText(/component3/)).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index f62749ee17..b507b96f05 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -13,16 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { Entity } from '@backstage/catalog-model'; -import { Table, TableColumn } from '@backstage/core'; +import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Table, TableColumn, TableProps } from '@backstage/core'; import { Link } from '@material-ui/core'; +import Edit from '@material-ui/icons/Edit'; +import GitHub from '@material-ui/icons/GitHub'; +import Star from '@material-ui/icons/Star'; +import StarOutline from '@material-ui/icons/StarBorder'; import { Alert } from '@material-ui/lab'; -import React, { FC } from 'react'; +import React from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; +import { findLocationForEntityMeta } from '../../data/utils'; +import { useStarredEntities } from '../../hooks/useStarredEntites'; import { entityRoute } from '../../routes'; -const columns: TableColumn[] = [ +const columns: TableColumn[] = [ { title: 'Name', field: 'metadata.name', @@ -63,16 +68,16 @@ type CatalogTableProps = { titlePreamble: string; loading: boolean; error?: any; - actions?: any; }; -export const CatalogTable: FC = ({ +export const CatalogTable = ({ entities, loading, error, titlePreamble, - actions, -}) => { +}: CatalogTableProps) => { + const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); + if (error) { return (
@@ -83,8 +88,57 @@ export const CatalogTable: FC = ({ ); } + const actions: TableProps['actions'] = [ + (rowData: Entity) => { + const location = findLocationForEntityMeta(rowData.metadata); + return { + icon: () => , + tooltip: 'View on GitHub', + onClick: () => { + if (!location) return; + window.open(location.target, '_blank'); + }, + hidden: location?.type !== 'github', + }; + }, + (rowData: Entity) => { + const createEditLink = (location: LocationSpec): string => { + switch (location.type) { + case 'github': + return location.target.replace('/blob/', '/edit/'); + default: + return location.target; + } + }; + const location = findLocationForEntityMeta(rowData.metadata); + return { + icon: () => , + tooltip: 'Edit', + onClick: () => { + if (!location) return; + window.open(createEditLink(location), '_blank'); + }, + hidden: location?.type !== 'github', + }; + }, + (rowData: Entity) => { + const isStarred = isStarredEntity(rowData); + return { + cellStyle: { paddingLeft: '1em' }, + icon: () => + isStarred ? ( + + ) : ( + + ), + tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites', + onClick: () => toggleStarredEntity(rowData), + }; + }, + ]; + return ( - isLoading={loading} columns={columns} options={{ diff --git a/plugins/catalog/src/data/filters.ts b/plugins/catalog/src/data/filters.ts deleted file mode 100644 index 890b267b4f..0000000000 --- a/plugins/catalog/src/data/filters.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 { Entity } from '@backstage/catalog-model'; -import SettingsIcon from '@material-ui/icons/Settings'; -import StarIcon from '@material-ui/icons/Star'; -import { - CatalogFilterGroup, - CatalogFilterItem, -} from '../components/CatalogFilter/CatalogFilter'; - -export enum EntityGroup { - ALL = 'ALL', - STARRED = 'STARRED', - OWNED = 'OWNED', -} - -export const filterGroups: CatalogFilterGroup[] = [ - { - name: 'Personal', - items: [ - { - id: EntityGroup.OWNED, - label: 'Owned', - icon: SettingsIcon, - }, - { - id: EntityGroup.STARRED, - label: 'Starred', - icon: StarIcon, - }, - ], - }, - { - // TODO: Replace with Company name, read from app config. - name: 'Company', - items: [ - { - id: EntityGroup.ALL, - label: 'All', - }, - ], - }, -]; - -export const getCatalogFilterItemByType = (filterType: EntityGroup) => { - for (const group of filterGroups) { - for (const filter of group.items) { - if (filter.id === filterType) { - return filter; - } - } - } - return null; -}; - -type EntityFilter = (entity: Entity, options: EntityFilterOptions) => boolean; - -export type EntityFilterOptions = { - isStarred: (entity: Entity) => boolean; - userId: string; -}; - -export const entityFilters: Record = { - [EntityGroup.OWNED]: (e, { userId }) => e.spec?.owner === userId, - [EntityGroup.ALL]: () => true, - [EntityGroup.STARRED]: (e, { isStarred }) => isStarred(e), -}; - -export const entityTypeFilter = (e: Entity, type: string) => - (e.spec as any)?.type === type; - -type EntityType = 'service' | 'website' | 'library' | 'documentation' | 'other'; - -export type LabeledEntityType = { - id: EntityType; - label: string; -}; - -export const labeledEntityTypes: LabeledEntityType[] = [ - { - id: 'service', - label: 'Services', - }, - { - id: 'website', - label: 'Websites', - }, - { - id: 'library', - label: 'Libraries', - }, - { - id: 'documentation', - label: 'Documentation', - }, - { - id: 'other', - label: 'Other', - }, -]; - -export const defaultFilter: CatalogFilterItem = filterGroups[0].items[0]; diff --git a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx index e8c307ebf6..dd557a4322 100644 --- a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx +++ b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx @@ -113,6 +113,8 @@ function useProvideEntityFilters(): FilterGroupsContext { register, unregister, setGroupSelectedFilters, + loading: !error && !entities, + error, filterGroupStates, matchingEntities, }; diff --git a/plugins/catalog/src/filter/context.ts b/plugins/catalog/src/filter/context.ts index 0f51f11b9a..31e9c91b97 100644 --- a/plugins/catalog/src/filter/context.ts +++ b/plugins/catalog/src/filter/context.ts @@ -26,6 +26,8 @@ export type FilterGroupsContext = { ) => void; unregister: (filterGroupId: string) => void; setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void; + loading: boolean; + error?: Error; filterGroupStates: { [filterGroupId: string]: FilterGroupStates }; matchingEntities: Entity[]; }; diff --git a/plugins/catalog/src/filter/useFilteredEntities.ts b/plugins/catalog/src/filter/useFilteredEntities.ts index 3f3bd03858..256f5247c7 100644 --- a/plugins/catalog/src/filter/useFilteredEntities.ts +++ b/plugins/catalog/src/filter/useFilteredEntities.ts @@ -27,8 +27,8 @@ export function useFilteredEntities() { } return { - loading: false, - error: undefined, + loading: context.loading, + error: context.error, matchingEntities: context.matchingEntities, }; }