diff --git a/.changeset/breezy-books-matter.md b/.changeset/breezy-books-matter.md new file mode 100644 index 0000000000..1bc206bdc5 --- /dev/null +++ b/.changeset/breezy-books-matter.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Fix empty state not being displayed on missing results. diff --git a/.changeset/fair-knives-relax.md b/.changeset/fair-knives-relax.md new file mode 100644 index 0000000000..bc1357d841 --- /dev/null +++ b/.changeset/fair-knives-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Introducing new UnhandledErrorForwarder installed by default. For catching unhandled promise rejections, you can override the API to align with general error handling. diff --git a/.changeset/great-ads-yawn.md b/.changeset/great-ads-yawn.md new file mode 100644 index 0000000000..3271dde108 --- /dev/null +++ b/.changeset/great-ads-yawn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Allow `defaultKind` from `CatalogTable.column.creatNameColumn` to be configurable diff --git a/.changeset/red-apricots-perform.md b/.changeset/red-apricots-perform.md new file mode 100644 index 0000000000..3d27381e6f --- /dev/null +++ b/.changeset/red-apricots-perform.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +chore: bump `del` from 5.1.0 to 6.0.0 diff --git a/.changeset/search-tasty-crews-tie.md b/.changeset/search-tasty-crews-tie.md new file mode 100644 index 0000000000..3d5b1ad1b0 --- /dev/null +++ b/.changeset/search-tasty-crews-tie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Handle errors in collators and decorators and log them. diff --git a/.changeset/short-numbers-carry.md b/.changeset/short-numbers-carry.md new file mode 100644 index 0000000000..0b2fbe80ae --- /dev/null +++ b/.changeset/short-numbers-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Fixed bug preventing searches with filter values containing `:` from returning results. diff --git a/.changeset/sour-donuts-tickle.md b/.changeset/sour-donuts-tickle.md new file mode 100644 index 0000000000..5cc3323662 --- /dev/null +++ b/.changeset/sour-donuts-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Adds an optional `actions` prop to `CatalogTable` and `CatalogPage` to support supplying custom actions for each entity row in the table. This uses the default actions if not provided. diff --git a/.changeset/tender-trees-notice.md b/.changeset/tender-trees-notice.md new file mode 100644 index 0000000000..cf8c395a2d --- /dev/null +++ b/.changeset/tender-trees-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Move `EntityTypePicker` to be consistent with `CatalogPage` and remove `api:` prefix from entity names diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 78b2a9b75f..906cbbb2a3 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -173,6 +173,7 @@ oidc Okta onboarding Onboarding +orgs pagerduty pageview parallelization diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 7422fa03ec..bc26a8c89a 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -4,7 +4,7 @@ The Backstage backend APIs are by default available without authentication. To a API requests from frontend plugins include an authorization header with a Backstage identity token acquired when the user logs in. By adding a middleware that verifies said token to be valid and signed by Backstage, non-authenticated requests can be blocked with a 401 Unauthorized response. -Note that this means Backstage will stop working for guests, as no token is issued for them. +**NOTE**: Enabling this means that Backstage will stop working for guests, as no token is issued for them. As techdocs HTML pages load assets without an Authorization header the code below also sets a token cookie when the user logs in (and when the token is about to expire). @@ -181,3 +181,83 @@ const app = createApp({ // ... ``` + +**NOTE**: Most Backstage frontend plugins come with the support for the `IdentityApi`. +In case you already have a dozen of internal ones, you may need to update those too. +Assuming you follow the common plugin structure, the changes to your front-end may look like: + +```diff +// plugins/internal-plugin/src/api.ts +- import {createApiRef} from '@backstage/core'; ++ import {createApiRef, IdentityApi} from '@backstage/core'; +import {Config} from '@backstage/config'; +// ... + +type MyApiOptions = { + configApi: Config; ++ identityApi: IdentityApi; + // ... +} + +interface MyInterface { + getData(): Promise; +} + +export class MyApi implements MyInterface { + private configApi: Config; ++ private identityApi: IdentityApi; + // ... + + constructor(options: MyApiOptions) { + this.configApi = options.configApi; ++ this.identityApi = options.identityApi; + } + + async getMyData() { + const backendUrl = this.configApi.getString('backend.baseUrl'); + ++ const token = await this.identityApi.getIdToken(); + const requestUrl = `${backendUrl}/api/data/`; +- const response = await fetch(requestUrl); ++ const response = await fetch( + requestUrl, + { headers: { Authorization: `Bearer ${token}` } }, + ); + // ... + } +``` + +and + +```diff +// plugins/internal-plugin/src/plugin.ts + +import { + configApiRef, + createApiFactory, + createPlugin, ++ identityApiRef, +} from '@backstage/core'; +import {mypluginPageRouteRef} from './routeRefs'; +import {MyApi, myApiRef} from './api'; + +export const plugin = createPlugin({ + id: 'my-plugin', + routes: { + mainPage: mypluginPageRouteRef, + }, + apis: [ + createApiFactory({ + api: myApiRef, + deps: { + configApi: configApiRef, ++ identityApi: identityApiRef, + }, +- factory: ({configApi}) => +- new MyApi({ configApi }), ++ factory: ({configApi, identityApi}) => ++ new MyApi({ configApi, identityApi }), + }), + ], +}); +``` diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index 308544e0f1..56dd46794a 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -71,8 +71,11 @@ For example, this is the default `ApiFactory` for the `ErrorApi`: createApiFactory({ api: errorApiRef, deps: { alertApi: alertApiRef }, - factory: ({ alertApi }) => - new ErrorAlerter(alertApi, new ErrorApiForwarder()), + factory: ({ alertApi }) => { + const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); + UnhandledErrorForwarder.forward(errorApi, { hidden: false }); + return errorApi; + }, }); ``` diff --git a/microsite/blog/2021-06-22-spotify-backstage-is-growing.md b/microsite/blog/2021-06-22-spotify-backstage-is-growing.md new file mode 100644 index 0000000000..58804242e1 --- /dev/null +++ b/microsite/blog/2021-06-22-spotify-backstage-is-growing.md @@ -0,0 +1,75 @@ +--- +title: How Spotify is helping more companies adopt Backstage +author: Austin Lamon, Spotify +authorURL: https://www.linkedin.com/in/austinlamon +--- + +[![Spotify Backstage: Proven at Spotify. Made just for you.](assets/21-06-22/spotify-backstage-header.gif)](https://backstage.spotify.com/) +_[backstage.spotify.com](https://backstage.spotify.com)_ + +The Backstage community is growing! In just over [a year](https://engineering.atspotify.com/2021/03/16/happy-birthday-backstage-spotifys-biggest-open-source-project-grows-up-fast/), Backstage has gone from a few open source building blocks to a thriving platform used by engineering orgs with thousands of developers. But even with 30+ [adopting companies](https://github.com/backstage/backstage/blob/master/ADOPTERS.md) and 400+ contributors, we are still in the very early stages of reaching the platform’s potential. + +In order to grow Backstage further, Spotify is increasing the support we provide both adopters (the people integrating Backstage into their organizations) and contributors (the people building features and improving the code). The more companies that adopt Backstage, the more support the project gets, the stronger the platform becomes for everyone. + +And while Spotify remains committed to maturing the Backstage platform — both as original creator and active maintainer — we also want to make room for the community to take greater ownership. Backstage may have started inside Spotify, but it belongs to all of you. So, we hope you join us in what’s next. + + + +## What’s next: More support for adopters and contributors + +Alongside the code contributions, technical support, and community leadership provided by our dedicated (and still growing) Backstage team, Spotify is introducing three additional ways to help lower the barriers to adopting the platform: + +1. **New consulting support.** In addition to investing in the Backstage getting started experience and the technical support we already provide, we’re adding [consulting support](https://backstage.spotify.com) for companies who are looking to adopt (or are already in the middle of adopting) Backstage. +2. **Double the community sessions.** We are creating separate meetups for Backstage adopters and Backstage contributors for more focused discussions. (Come to both!) +3. **Adding reviewers and maintainers.** We recently introduced [reviewers](https://github.com/backstage/backstage/pull/5137) to the Backstage project to speed up PR reviews and approvals, with the hope of also adding more maintainers in the future. + +## Why Spotify is increasing its investment in open source (and why now) + +Before we talk in more detail about these new efforts, why is Spotify doing this? The short answer is the same answer as when we released the very first open source version of Backstage: we envision Backstage as the standard developer portal platform across the industry. + +### Setting the standard, both inside and outside Spotify + +We believe in Backstage — we believe in the developer experience it provides, the developer-centric culture it encourages, and the immense value that the open source community brings to it. It is no exaggeration to say that we depend on Backstage every day at Spotify. It’s the central hub for our internal R&D community, and it’s both mission-critical to our daily operations and our future growth. + +### We’re an adopter, too + +We (that includes Spotify’s leadership, as well as our platform teams and dedicated Backstage team) also believe that Backstage’s continued success here — inside Spotify — depends on its success out here — in the wider open source community, where Backstage can reach its full potential. Like other adopters, we’re fully invested in the platform’s growth. + +### An open platform is the strongest platform + +We genuinely believe that the best platform for developers can only be shaped by the most diverse group of developers. Each new adopter and every new contributor brings unique perspectives and experiences to the challenges of improving developer experience and effectiveness. As the project scales — and progresses toward CNCF graduation — we need to make more room in the community for both adopters and contributors. + +So, let’s get to it. + +## Consulting support (and a new website) for adopters + +[![Who else is using Backstage? Netflix, Zalando, TELUS, DoorDash, more](assets/21-06-22/spotify-backstage-adopters.png)](https://backstage.spotify.com/) + +We’ve launched a new website at: [backstage.spotify.com](https://backstage.spotify.com). It’s a hub for new and potential adopters to receive support from Spotify and our Preferred Partners. The site is focused on helping organizations get up and running with Backstage by addressing their unique needs and use cases. + +You’ll find a high-level introduction to the platform, tips and tricks tested by Spotify to accelerate developer effectiveness, and access to a group of partners that have scaled Backstage for numerous adopters. You can also use the site to book product overviews, demos, and technical deep dives with members of the Spotify team. + +We will continue to post important product announcements, technical documentation, feature demos, and community news here on Backstage.io. ([Subscribe to the newsletter](https://mailchi.mp/spotify/backstage-community) to stay up to date.) And both contributors and adopting companies can continue to find around-the-clock/around-the-world technical support on [GitHub](https://github.com/backstage/backstage) and [Discord](https://discord.gg/MUpMjP2). + +## Separate community sessions for adopters and contributors + +[![Backstage Community Sessions, hosted by Spotify](assets/21-06-22/backstage-community-sessions.png)](https://github.com/backstage/community/#backstage-community) + +Earlier this year, we began hosting [Backstage Community Sessions](https://github.com/backstage/community/#backstage-community) — official meetups for anyone who wanted to join them. Since [the very first one](https://youtu.be/4-VX9tDdJYY), the Backstage team has been inspired and humbled by the community’s participation in these sessions — from hearing the [Expedia Group team share their journey adopting Backstage](https://youtu.be/rRphwXeq33Q?t=1509) to discussions about TypeScript and Material-UI. It’s great collaborating through code — but it’s also a lot of fun when you can see each other’s faces and have a conversation. + +And while these sessions have been a success, the feedback we’ve gotten from the community has been very clear: more frequent and more focused conversations. So, later this summer, we’ll be launching standalone Backstage Adopter Sessions and Backstage Contributor Sessions. We hope this will lead to more useful sessions for everyone — and, of course, you are welcome to attend either or both: + +- **For the adopter sessions:** we invite you to share the challenges, learnings, and use cases you’re facing with companies similar to yourselves. +- **For the contributor sessions:** we invite you to share thoughts, suggestions, and gaps in the Backstage core with the maintainers and reviewers. + +Speaking of reviewers and maintainers… + +## Adding reviewers and maintainers + +[![GitHub logo](assets/21-06-22/gh-reviewers.png)](https://github.com/backstage/backstage/blob/master/GOVERNANCE.md#reviewers) + +We have introduced [reviewers](https://github.com/backstage/backstage/blob/master/GOVERNANCE.md#reviewers) to the project! By adding this new role, we’ve expanded the number of people who are permitted to approve and merge pull requests. This will offload some of the review work from the maintainers, simplifying and speeding up the review process for contributors. + +Of course, with these new efforts, we expect even more companies to adopt Backstage, which means the platform will continue to grow, and the number of PRs will continue to grow with it. As that happens, we hope to add to both the maintainer and reviewer teams in the future. + +So, I’ll end this post as it began: the Backstage community is growing! And we look forward to growing even bigger, even faster, together. diff --git a/microsite/blog/assets/21-06-22/backstage-community-sessions.png b/microsite/blog/assets/21-06-22/backstage-community-sessions.png new file mode 100644 index 0000000000..ccfaf6498e Binary files /dev/null and b/microsite/blog/assets/21-06-22/backstage-community-sessions.png differ diff --git a/microsite/blog/assets/21-06-22/gh-reviewers.png b/microsite/blog/assets/21-06-22/gh-reviewers.png new file mode 100644 index 0000000000..c7402525a8 Binary files /dev/null and b/microsite/blog/assets/21-06-22/gh-reviewers.png differ diff --git a/microsite/blog/assets/21-06-22/spotify-backstage-adopters.png b/microsite/blog/assets/21-06-22/spotify-backstage-adopters.png new file mode 100644 index 0000000000..2bd963f91a Binary files /dev/null and b/microsite/blog/assets/21-06-22/spotify-backstage-adopters.png differ diff --git a/microsite/blog/assets/21-06-22/spotify-backstage-header.gif b/microsite/blog/assets/21-06-22/spotify-backstage-header.gif new file mode 100644 index 0000000000..93c4b5857b Binary files /dev/null and b/microsite/blog/assets/21-06-22/spotify-backstage-header.gif differ diff --git a/packages/backend/README.md b/packages/backend/README.md index d458268846..97890f72cb 100644 --- a/packages/backend/README.md +++ b/packages/backend/README.md @@ -6,9 +6,7 @@ The main purpose of this package is to provide a test bed for Backstage plugins that have a backend part. Feel free to experiment locally or within your fork by adding dependencies and routes to this backend, to try things out. -Our goal is to eventually amend the create-app flow of the CLI, such that a -production ready version of a backend skeleton is made alongside the frontend -app. Until then, feel free to experiment here! +By running the `@backstage/create-app` script, you get your own separate Backstage backend. ## Development diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9ea2e89af3..168f6aa2d9 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -71,9 +71,16 @@ function makeCreateEnv(config: Config) { } async function main() { + const logger = getRootLogger(); + + logger.info( + `You are running an example backend, which is supposed to be mainly used for contributing back to Backstage. ` + + `Do NOT deploy this to production. Read more here https://backstage.io/docs/getting-started/`, + ); + const config = await loadBackendConfig({ argv: process.argv, - logger: getRootLogger(), + logger, }); const createEnv = makeCreateEnv(config); @@ -118,7 +125,7 @@ async function main() { .addRouter('', await app(appEnv)); await service.start().catch(err => { - console.log(err); + logger.error(err); process.exit(1); }); } diff --git a/packages/cli/package.json b/packages/cli/package.json index 202705cb39..1d8628e771 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -143,7 +143,7 @@ "@types/webpack": "^4.41.7", "@types/webpack-dev-server": "^3.11.0", "@types/yarnpkg__lockfile": "^1.1.4", - "del": "^5.1.0", + "del": "^6.0.0", "mock-fs": "^4.13.0", "nodemon": "^2.0.2", "ts-node": "^9.1.1" diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 5aa709121a..ce85078a88 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -394,6 +394,11 @@ export type SignInResult = { signOut?: () => Promise; }; +// @public (undocumented) +export class UnhandledErrorForwarder { + static forward(errorApi: ErrorApi, errorContext: ErrorContext): void; +} + // @public export class UrlPatternDiscovery implements DiscoveryApi { static compile(pattern: string): UrlPatternDiscovery; diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts new file mode 100644 index 0000000000..b859148e8e --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/UnhandledErrorForwarder.ts @@ -0,0 +1,31 @@ +import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; + +/* + * 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 class UnhandledErrorForwarder { + /** + * Add event listener, such that unhandled errors can be forwarded using an given `ErrorApi` instance + */ + static forward(errorApi: ErrorApi, errorContext: ErrorContext) { + window.addEventListener( + 'unhandledrejection', + (e: PromiseRejectionEvent) => { + errorApi.post(e.reason as Error, errorContext); + }, + ); + } +} diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/index.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/index.ts index 49e12b4646..0d82894060 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/index.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/index.ts @@ -16,3 +16,4 @@ export { ErrorAlerter } from './ErrorAlerter'; export { ErrorApiForwarder } from './ErrorApiForwarder'; +export { UnhandledErrorForwarder } from './UnhandledErrorForwarder'; diff --git a/packages/core-app-api/src/app/defaultApis.ts b/packages/core-app-api/src/app/defaultApis.ts index 815418ac6c..2322f72e8d 100644 --- a/packages/core-app-api/src/app/defaultApis.ts +++ b/packages/core-app-api/src/app/defaultApis.ts @@ -30,6 +30,7 @@ import { UrlPatternDiscovery, SamlAuth, OneLoginAuth, + UnhandledErrorForwarder, } from '../apis'; import { @@ -67,8 +68,11 @@ export const defaultApis = [ createApiFactory({ api: errorApiRef, deps: { alertApi: alertApiRef }, - factory: ({ alertApi }) => - new ErrorAlerter(alertApi, new ErrorApiForwarder()), + factory: ({ alertApi }) => { + const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); + UnhandledErrorForwarder.forward(errorApi, { hidden: false }); + return errorApi; + }, }), createApiFactory({ api: storageApiRef, diff --git a/packages/core-app-api/src/index.test.ts b/packages/core-app-api/src/index.test.ts index 55ed5c7a38..1d17e78402 100644 --- a/packages/core-app-api/src/index.test.ts +++ b/packages/core-app-api/src/index.test.ts @@ -37,6 +37,7 @@ describe('index', () => { ConfigReader: expect.any(Function), ErrorAlerter: expect.any(Function), ErrorApiForwarder: expect.any(Function), + UnhandledErrorForwarder: expect.any(Function), FeatureFlagged: expect.any(Function), GithubAuth: expect.any(Function), GitlabAuth: expect.any(Function), diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index 0660ed09db..9a8b725ffc 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -48,6 +48,16 @@ const useStyles = makeStyles(theme => ({ }, })); +const defaultColumns: TableColumn[] = [ + 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[]; @@ -80,13 +90,13 @@ export const ApiExplorerPage = ({
- +
diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index b43606481e..584eea0850 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -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(); + + 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 2f54ffd240..ba19d6036c 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -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[]; + 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 c43fc34d62..bde116ea09 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -52,9 +52,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,7 +76,7 @@ export const CatalogTable = ({ columns }: CatalogTableProps) => { ); } - const actions: TableProps['actions'] = [ + const defaultActions: TableProps['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} /> ); }; diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 38431476ce..60493d1847 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -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 { +type NameColumnProps = { + defaultKind?: string; +}; + +export function createNameColumn( + props?: NameColumnProps, +): TableColumn { return { title: 'Name', field: 'resolved.name', highlight: true, render: ({ entity }) => ( - + ), }; } diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 0a852343d8..f2f28679a5 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -41,3 +41,4 @@ export { EntityLinksCard, EntitySystemDiagramCard, } from './plugin'; +export * from './components/CatalogTable/columns'; diff --git a/plugins/search-backend-node/src/IndexBuilder.ts b/plugins/search-backend-node/src/IndexBuilder.ts index 5b446c6483..0cabb61f38 100644 --- a/plugins/search-backend-node/src/IndexBuilder.ts +++ b/plugins/search-backend-node/src/IndexBuilder.ts @@ -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) { diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index b09777217a..22c775da8e 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -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 = [ { diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index f2ace54ed2..a1c12f019d 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -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}`; } diff --git a/plugins/search/src/components/SearchResult/SearchResult.test.tsx b/plugins/search/src/components/SearchResult/SearchResult.test.tsx index 0a18690353..9508eec259 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.test.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.test.tsx @@ -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({() => <>}); + + 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: [] } }, diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index 6ef6d533a7..7ca5964d21 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -41,7 +41,7 @@ const SearchResultComponent = ({ children }: Props) => { ); } - if (!value) { + if (!value?.results.length) { return ; } diff --git a/yarn.lock b/yarn.lock index ebe465987f..10a960a9ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11160,18 +11160,18 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" -del@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7" - integrity sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA== +del@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" + integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== dependencies: - globby "^10.0.1" - graceful-fs "^4.2.2" + globby "^11.0.1" + graceful-fs "^4.2.4" is-glob "^4.0.1" is-path-cwd "^2.2.0" - is-path-inside "^3.0.1" - p-map "^3.0.0" - rimraf "^3.0.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" slash "^3.0.0" delay@^5.0.0: @@ -12819,7 +12819,7 @@ fast-glob@^2.0.2, fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.0.3, fast-glob@^3.1.1: +fast-glob@^3.1.1: version "3.2.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== @@ -13905,7 +13905,7 @@ globby@11.0.1: merge2 "^1.3.0" slash "^3.0.0" -globby@11.0.3, globby@^11.0.0, globby@^11.0.2, globby@^11.0.3: +globby@11.0.3: version "11.0.3" resolved "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== @@ -13930,18 +13930,16 @@ globby@8.0.2: pify "^3.0.0" slash "^1.0.0" -globby@^10.0.1: - version "10.0.2" - resolved "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" - integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== +globby@^11.0.0, globby@^11.0.1, globby@^11.0.2, globby@^11.0.3: + version "11.0.4" + resolved "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" + integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== dependencies: - "@types/glob" "^7.1.1" array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.0.3" - glob "^7.1.3" - ignore "^5.1.1" - merge2 "^1.2.3" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" slash "^3.0.0" globby@^6.1.0: @@ -14085,12 +14083,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.5, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: - version "4.2.4" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -graceful-fs@^4.2.3: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.5, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4: version "4.2.6" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -14928,7 +14921,7 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1, ignore@^5.1.4: +ignore@^5.1.4: version "5.1.4" resolved "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== @@ -15618,12 +15611,7 @@ is-path-inside@^2.1.0: dependencies: path-is-inside "^1.0.2" -is-path-inside@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" - integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== - -is-path-inside@^3.0.2: +is-path-inside@^3.0.1, is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==