Merge branch 'master' into rugvip/mod

This commit is contained in:
Patrik Oldsberg
2021-06-22 19:51:42 +02:00
committed by GitHub
36 changed files with 471 additions and 57 deletions
@@ -48,6 +48,16 @@ const useStyles = makeStyles(theme => ({
},
}));
const defaultColumns: TableColumn<CatalogTableRow>[] = [
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
CatalogTable.columns.createSystemColumn(),
CatalogTable.columns.createOwnerColumn(),
CatalogTable.columns.createSpecTypeColumn(),
CatalogTable.columns.createSpecLifecycleColumn(),
CatalogTable.columns.createMetadataDescriptionColumn(),
CatalogTable.columns.createTagsColumn(),
];
export type ApiExplorerPageProps = {
initiallySelectedFilter?: UserListFilterKind;
columns?: TableColumn<CatalogTableRow>[];
@@ -80,13 +90,13 @@ export const ApiExplorerPage = ({
<EntityListProvider>
<div>
<EntityKindPicker initialFilter="api" hidden />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</div>
<CatalogTable columns={columns} />
<CatalogTable columns={columns || defaultColumns} />
</EntityListProvider>
</div>
</Content>
@@ -20,6 +20,7 @@ import {
RELATION_MEMBER_OF,
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import { TableColumn, TableProps } from '@backstage/core-components';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import {
MockStorageApi,
@@ -29,7 +30,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';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import {
@@ -128,6 +131,81 @@ describe('CatalogPage', () => {
),
);
it('should render the default column of the grid', async () => {
const { getAllByRole } = await renderWrapped(<CatalogPage />);
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<EntityRow>[] = [
{ title: 'Foo', field: 'entity.foo' },
{ title: 'Bar', field: 'entity.bar' },
{ title: 'Baz', field: 'entity.spec.lifecycle' },
];
const { getAllByRole } = await renderWrapped(
<CatalogPage columns={columns} />,
);
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(<CatalogPage />);
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<EntityRow>['actions'] = [
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Foo Action',
disabled: false,
onClick: () => jest.fn(),
};
},
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Bar Action',
disabled: true,
onClick: () => jest.fn(),
};
},
];
const { findByTitle, findByText } = await renderWrapped(
<CatalogPage actions={actions} />,
);
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
@@ -36,6 +36,7 @@ import {
ContentHeader,
SupportButton,
TableColumn,
TableProps,
} from '@backstage/core-components';
const useStyles = makeStyles(theme => ({
@@ -53,11 +54,13 @@ const useStyles = makeStyles(theme => ({
export type CatalogPageProps = {
initiallySelectedFilter?: UserListFilterKind;
columns?: TableColumn<EntityRow>[];
actions?: TableProps<EntityRow>['actions'];
};
export const CatalogPage = ({
initiallySelectedFilter = 'owned',
columns,
actions,
}: CatalogPageProps) => {
const styles = useStyles();
@@ -78,7 +81,7 @@ export const CatalogPage = ({
<EntityLifecyclePicker />
<EntityTagPicker />
</div>
<CatalogTable columns={columns} />
<CatalogTable columns={columns} actions={actions} />
</EntityListProvider>
</div>
</Content>
@@ -52,9 +52,10 @@ const defaultColumns: TableColumn<EntityRow>[] = [
type CatalogTableProps = {
columns?: TableColumn<EntityRow>[];
actions?: TableProps<EntityRow>['actions'];
};
export const CatalogTable = ({ columns }: CatalogTableProps) => {
export const CatalogTable = ({ columns, actions }: CatalogTableProps) => {
const { isStarredEntity, toggleStarredEntity } = useStarredEntities();
const { loading, error, entities, filters } = useEntityListProvider();
@@ -75,7 +76,7 @@ export const CatalogTable = ({ columns }: CatalogTableProps) => {
);
}
const actions: TableProps<EntityRow>['actions'] = [
const defaultActions: TableProps<EntityRow>['actions'] = [
({ entity }) => {
const url = getEntityMetadataViewUrl(entity);
return {
@@ -159,7 +160,7 @@ export const CatalogTable = ({ columns }: CatalogTableProps) => {
}}
title={`${titlePreamble} (${entities.length})`}
data={rows}
actions={actions}
actions={actions || defaultActions}
/>
);
};
@@ -19,13 +19,22 @@ import { Chip } from '@material-ui/core';
import { EntityRow } from './types';
import { OverflowTooltip, TableColumn } from '@backstage/core-components';
export function createNameColumn(): TableColumn<EntityRow> {
type NameColumnProps = {
defaultKind?: string;
};
export function createNameColumn(
props?: NameColumnProps,
): TableColumn<EntityRow> {
return {
title: 'Name',
field: 'resolved.name',
highlight: true,
render: ({ entity }) => (
<EntityRefLink entityRef={entity} defaultKind="Component" />
<EntityRefLink
entityRef={entity}
defaultKind={props?.defaultKind || 'Component'}
/>
),
};
}
+1
View File
@@ -41,3 +41,4 @@ export {
EntityLinksCard,
EntitySystemDiagramCard,
} from './plugin';
export * from './components/CatalogTable/columns';
@@ -14,7 +14,11 @@
* limitations under the License.
*/
import { DocumentCollator, DocumentDecorator } from '@backstage/search-common';
import {
DocumentCollator,
DocumentDecorator,
IndexableDocument,
} from '@backstage/search-common';
import { Logger } from 'winston';
import { Scheduler } from './index';
import {
@@ -105,12 +109,29 @@ export class IndexBuilder {
this.logger.debug(
`Collating documents for ${type} via ${this.collators[type].collate.constructor.name}`,
);
let documents = await this.collators[type].collate.execute();
let documents: IndexableDocument[];
try {
documents = await this.collators[type].collate.execute();
} catch (e) {
this.logger.error(
`Collating documents for ${type} via ${this.collators[type].collate.constructor.name} failed: ${e}`,
);
return;
}
for (let i = 0; i < decorators.length; i++) {
this.logger.debug(
`Decorating ${type} documents via ${decorators[i].constructor.name}`,
);
documents = await decorators[i].execute(documents);
try {
documents = await decorators[i].execute(documents);
} catch (e) {
this.logger.error(
`Decorating ${type} documents via ${decorators[i].constructor.name} failed: ${e}`,
);
return;
}
}
if (!documents || documents.length === 0) {
@@ -339,6 +339,46 @@ describe('LunrSearchEngine', () => {
});
});
it('should perform search query and return search results on match with filters that include a : character', async () => {
const mockDocuments = [
{
title: 'testTitle',
text: 'testText',
location: 'test:location',
},
{
title: 'testTitle',
text: 'testText',
location: 'test:location2',
},
];
// Mock indexing of 2 documents
testLunrSearchEngine.index('test-index', mockDocuments);
// Perform search query
const mockedSearchResult = await testLunrSearchEngine.query({
term: 'testTitle',
filters: { location: 'test:location2' },
pageCursor: '',
});
// Should return 1 of 2 results as we are
// 1. Mocking the indexing of 2 documents
// 2. Matching on the location field with the filter { location: 'test:location2' }
expect(mockedSearchResult).toMatchObject({
results: [
{
document: {
title: 'testTitle',
text: 'testText',
location: 'test:location2',
},
},
],
});
});
it('should perform search query and return search results on match, scoped to specific index', async () => {
const mockDocuments = [
{
@@ -57,6 +57,9 @@ export class LunrSearchEngine implements SearchEngine {
.map(([field, value]) => {
// Require that the given field has the given value (with +).
if (['string', 'number', 'boolean'].includes(typeof value)) {
if (typeof value === 'string') {
return ` +${field}:${value.replace(':', '\\:')}`;
}
return ` +${field}:${value}`;
}
@@ -55,7 +55,7 @@ describe('SearchResult', () => {
});
});
it('On empty result value state', async () => {
it('On no result value state', async () => {
(useSearch as jest.Mock).mockReturnValueOnce({
result: { loading: false, error: '', value: undefined },
});
@@ -69,6 +69,20 @@ describe('SearchResult', () => {
});
});
it('On empty result value state', async () => {
(useSearch as jest.Mock).mockReturnValueOnce({
result: { loading: false, error: '', value: { results: [] } },
});
const { getByRole } = render(<SearchResult>{() => <></>}</SearchResult>);
await waitFor(() => {
expect(
getByRole('heading', { name: 'Sorry, no results were found' }),
).toBeInTheDocument();
});
});
it('Calls children with results set to result.value', () => {
(useSearch as jest.Mock).mockReturnValueOnce({
result: { loading: false, error: '', value: { results: [] } },
@@ -41,7 +41,7 @@ const SearchResultComponent = ({ children }: Props) => {
);
}
if (!value) {
if (!value?.results.length) {
return <EmptyState missing="data" title="Sorry, no results were found" />;
}