Merge branch 'master' into rugvip/mod
This commit is contained in:
@@ -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'}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user