diff --git a/.changeset/sour-donuts-tickle.md b/.changeset/sour-donuts-tickle.md new file mode 100644 index 0000000000..a8035263eb --- /dev/null +++ b/.changeset/sour-donuts-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Add possibility to customize actions in CatalogTable diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index e6a1f1f687..a9cf96d02c 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -27,6 +27,8 @@ import { identityApiRef, ProfileInfo, storageApiRef, + TableColumn, + TableProps, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { @@ -37,7 +39,9 @@ import { import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { createComponentRouteRef } from '../../routes'; +import { EntityRow } from '../CatalogTable/types'; import { CatalogPage } from './CatalogPage'; +import DashboardIcon from '@material-ui/icons/Dashboard'; describe('CatalogPage', () => { const catalogApi: Partial = { @@ -128,6 +132,81 @@ describe('CatalogPage', () => { ), ); + it('should render the default column of the grid', async () => { + const { getAllByRole } = await renderWrapped(); + + const columnHeader = getAllByRole('button').filter( + c => c.tagName === 'SPAN', + ); + const columnHeaderLabels = columnHeader.map(c => c.textContent); + + expect(columnHeaderLabels).toEqual([ + 'Name', + 'System', + 'Owner', + 'Type', + 'Lifecycle', + 'Description', + 'Tags', + 'Actions', + ]); + }); + + it('should render the custom column passed as prop', async () => { + const columns: TableColumn[] = [ + { title: 'Foo', field: 'entity.foo' }, + { title: 'Bar', field: 'entity.bar' }, + { title: 'Baz', field: 'entity.spec.lifecycle' }, + ]; + const { getAllByRole } = await renderWrapped( + , + ); + + const columnHeader = getAllByRole('button').filter( + c => c.tagName === 'SPAN', + ); + const columnHeaderLabels = columnHeader.map(c => c.textContent); + + expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']); + }); + + it('should render the default actions of an item in the grid', async () => { + const { findByTitle, findByText } = await renderWrapped(); + expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(await findByTitle(/View/)).toBeInTheDocument(); + expect(await findByTitle(/Edit/)).toBeInTheDocument(); + expect(await findByTitle(/Add to favorites/)).toBeInTheDocument(); + }); + + it('should render the custom actions of an item passed as prop', async () => { + const actions: TableProps['actions'] = [ + () => { + return { + icon: () => , + tooltip: 'Foo Action', + disabled: false, + onClick: () => jest.fn(), + }; + }, + () => { + return { + icon: () => , + tooltip: 'Bar Action', + disabled: true, + onClick: () => jest.fn(), + }; + }, + ]; + + const { findByTitle, findByText } = await renderWrapped( + , + ); + expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(await findByTitle(/Foo Action/)).toBeInTheDocument(); + expect(await findByTitle(/Bar Action/)).toBeInTheDocument(); + expect((await findByTitle(/Bar Action/)).firstChild).toBeDisabled(); + }); + // 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 diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index d3ade03fe4..aa312f9b6a 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -21,6 +21,7 @@ import { ContentHeader, SupportButton, TableColumn, + TableProps, } from '@backstage/core'; import { EntityKindPicker, @@ -53,11 +54,13 @@ const useStyles = makeStyles(theme => ({ export type CatalogPageProps = { initiallySelectedFilter?: UserListFilterKind; columns?: TableColumn[]; + actions?: TableProps['actions']; }; export const CatalogPage = ({ initiallySelectedFilter = 'owned', columns, + actions, }: CatalogPageProps) => { const styles = useStyles(); @@ -78,7 +81,7 @@ export const CatalogPage = ({ - + diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 6d9f0c21bd..f7049ead9d 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -23,21 +23,14 @@ import { } from '@backstage/core'; import { formatEntityRefTitle, - getEntityMetadataEditUrl, - getEntityMetadataViewUrl, getEntityRelations, useEntityListProvider, useStarredEntities, } from '@backstage/plugin-catalog-react'; -import Edit from '@material-ui/icons/Edit'; -import OpenInNew from '@material-ui/icons/OpenInNew'; import { capitalize } from 'lodash'; import React from 'react'; -import { - favouriteEntityIcon, - favouriteEntityTooltip, -} from '../FavouriteEntity/FavouriteEntity'; import * as columnFactories from './columns'; +import * as actionFactories from './actions'; import { EntityRow } from './types'; const defaultColumns: TableColumn[] = [ @@ -52,9 +45,10 @@ const defaultColumns: TableColumn[] = [ type CatalogTableProps = { columns?: TableColumn[]; + actions?: TableProps['actions']; }; -export const CatalogTable = ({ columns }: CatalogTableProps) => { +export const CatalogTable = ({ columns, actions }: CatalogTableProps) => { const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); const { loading, error, entities, filters } = useEntityListProvider(); @@ -75,40 +69,15 @@ export const CatalogTable = ({ columns }: CatalogTableProps) => { ); } - const actions: TableProps['actions'] = [ - ({ entity }) => { - const url = getEntityMetadataViewUrl(entity); - return { - icon: () => , - tooltip: 'View', - disabled: !url, - onClick: () => { - if (!url) return; - window.open(url, '_blank'); - }, - }; - }, - ({ entity }) => { - const url = getEntityMetadataEditUrl(entity); - return { - icon: () => , - tooltip: 'Edit', - disabled: !url, - onClick: () => { - if (!url) return; - window.open(url, '_blank'); - }, - }; - }, - ({ entity }) => { - const isStarred = isStarredEntity(entity); - return { - cellStyle: { paddingLeft: '1em' }, - icon: () => favouriteEntityIcon(isStarred), - tooltip: favouriteEntityTooltip(isStarred), - onClick: () => toggleStarredEntity(entity), - }; - }, + const defaultActions: TableProps['actions'] = [ + ({ entity }) => actionFactories.createViewUrlAction(entity), + ({ entity }) => actionFactories.createEditUrlAction(entity), + ({ entity }) => + actionFactories.createStarredAction( + entity, + isStarredEntity, + toggleStarredEntity, + ), ]; const rows = entities.map(entity => { @@ -159,7 +128,7 @@ export const CatalogTable = ({ columns }: CatalogTableProps) => { }} title={`${titlePreamble} (${entities.length})`} data={rows} - actions={actions} + actions={actions || defaultActions} /> ); }; diff --git a/plugins/catalog/src/components/CatalogTable/actions.tsx b/plugins/catalog/src/components/CatalogTable/actions.tsx new file mode 100644 index 0000000000..dfbb0202ea --- /dev/null +++ b/plugins/catalog/src/components/CatalogTable/actions.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2021 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 { Entity } from '@backstage/catalog-model'; +import { + getEntityMetadataViewUrl, + getEntityMetadataEditUrl, +} from '@backstage/plugin-catalog-react'; +import OpenInNew from '@material-ui/icons/OpenInNew'; +import Edit from '@material-ui/icons/Edit'; +import { + favouriteEntityIcon, + favouriteEntityTooltip, +} from '../FavouriteEntity/FavouriteEntity'; + +export function createViewUrlAction(entity: Entity) { + const url = getEntityMetadataViewUrl(entity); + return { + icon: () => , + tooltip: 'View', + disabled: !url, + onClick: () => { + if (!url) return; + window.open(url, '_blank'); + }, + }; +} + +export function createEditUrlAction(entity: Entity) { + const url = getEntityMetadataEditUrl(entity); + return { + icon: () => , + tooltip: 'Edit', + disabled: !url, + onClick: () => { + if (!url) return; + window.open(url, '_blank'); + }, + }; +} + +export function createStarredAction( + entity: Entity, + isStarredEntity: (entity: Entity) => boolean, + toggleStarredEntity: (entity: Entity) => void, +) { + const isStarred = isStarredEntity(entity); + return { + cellStyle: { paddingLeft: '1em' }, + icon: () => favouriteEntityIcon(isStarred), + tooltip: favouriteEntityTooltip(isStarred), + onClick: () => toggleStarredEntity(entity), + }; +} diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 0a40d50036..249d636551 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -41,3 +41,6 @@ export { EntityLinksCard, EntitySystemDiagramCard, } from './plugin'; +export type { EntityRow } from './components/CatalogTable/types'; +export * from './components/CatalogTable/columns'; +export * from './components/CatalogTable/actions';