From 4dec6f16be04bd8f6005823db876b1f6d13a4dca Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Mon, 28 Nov 2022 13:43:28 -0500 Subject: [PATCH 1/9] feat: add explore-backend plugin Signed-off-by: Andrew Thauer --- .changeset/rude-sloths-cross.md | 8 ++ .../app/src/components/search/SearchModal.tsx | 17 +++ .../app/src/components/search/SearchPage.tsx | 12 ++ packages/backend/package.json | 2 + packages/backend/src/index.ts | 3 + packages/backend/src/plugins/explore.ts | 33 +++++ packages/backend/src/plugins/search.ts | 9 ++ plugins/explore-backend/.eslintrc.js | 1 + plugins/explore-backend/README.md | 93 ++++++++++++++ plugins/explore-backend/api-report.md | 59 +++++++++ plugins/explore-backend/package.json | 46 +++++++ .../src/example/getExampleTools.ts | 44 +++++++ plugins/explore-backend/src/example/tools.ts | 98 +++++++++++++++ plugins/explore-backend/src/index.ts | 30 +++++ plugins/explore-backend/src/run.ts | 33 +++++ .../src/search/ToolDocumentCollatorFactory.ts | 99 +++++++++++++++ plugins/explore-backend/src/search/index.ts | 17 +++ .../src/service/router.test.ts | 66 ++++++++++ plugins/explore-backend/src/service/router.ts | 59 +++++++++ .../src/service/standaloneServer.ts | 67 +++++++++++ plugins/explore-backend/src/service/types.ts | 33 +++++ plugins/explore-backend/src/setupTests.ts | 17 +++ plugins/explore-common/.eslintrc.js | 1 + plugins/explore-common/README.md | 3 + plugins/explore-common/api-report.md | 37 ++++++ plugins/explore-common/package.json | 39 ++++++ plugins/explore-common/src/api.ts | 46 +++++++ plugins/explore-common/src/index.ts | 25 ++++ plugins/explore-common/src/types.ts | 27 +++++ plugins/explore-react/api-report.md | 16 +-- plugins/explore-react/package.json | 3 +- plugins/explore-react/src/tools/api.ts | 31 +++-- plugins/explore/package.json | 4 + plugins/explore/src/api/client.ts | 66 ++++++++++ plugins/explore/src/api/index.ts | 19 +++ plugins/explore/src/api/types.ts | 39 ++++++ .../ToolSearchResultListItem.tsx | 113 ++++++++++++++++++ .../ToolSearchResultListItem/index.ts | 17 +++ plugins/explore/src/components/index.ts | 1 + plugins/explore/src/index.ts | 1 + plugins/explore/src/plugin.ts | 36 ++++-- yarn.lock | 72 +++++++++++ 42 files changed, 1411 insertions(+), 31 deletions(-) create mode 100644 .changeset/rude-sloths-cross.md create mode 100644 packages/backend/src/plugins/explore.ts create mode 100644 plugins/explore-backend/.eslintrc.js create mode 100644 plugins/explore-backend/README.md create mode 100644 plugins/explore-backend/api-report.md create mode 100644 plugins/explore-backend/package.json create mode 100644 plugins/explore-backend/src/example/getExampleTools.ts create mode 100644 plugins/explore-backend/src/example/tools.ts create mode 100644 plugins/explore-backend/src/index.ts create mode 100644 plugins/explore-backend/src/run.ts create mode 100644 plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts create mode 100644 plugins/explore-backend/src/search/index.ts create mode 100644 plugins/explore-backend/src/service/router.test.ts create mode 100644 plugins/explore-backend/src/service/router.ts create mode 100644 plugins/explore-backend/src/service/standaloneServer.ts create mode 100644 plugins/explore-backend/src/service/types.ts create mode 100644 plugins/explore-backend/src/setupTests.ts create mode 100644 plugins/explore-common/.eslintrc.js create mode 100644 plugins/explore-common/README.md create mode 100644 plugins/explore-common/api-report.md create mode 100644 plugins/explore-common/package.json create mode 100644 plugins/explore-common/src/api.ts create mode 100644 plugins/explore-common/src/index.ts create mode 100644 plugins/explore-common/src/types.ts create mode 100644 plugins/explore/src/api/client.ts create mode 100644 plugins/explore/src/api/index.ts create mode 100644 plugins/explore/src/api/types.ts create mode 100644 plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx create mode 100644 plugins/explore/src/components/ToolSearchResultListItem/index.ts diff --git a/.changeset/rude-sloths-cross.md b/.changeset/rude-sloths-cross.md new file mode 100644 index 0000000000..8e22070662 --- /dev/null +++ b/.changeset/rude-sloths-cross.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-explore': patch +'@backstage/plugin-explore-backend': patch +'@backstage/plugin-explore-common': patch +'@backstage/plugin-explore-react': patch +--- + +Updated to use new @backstage/plugin-explore-backend diff --git a/packages/app/src/components/search/SearchModal.tsx b/packages/app/src/components/search/SearchModal.tsx index 218ead9b82..2aed970e75 100644 --- a/packages/app/src/components/search/SearchModal.tsx +++ b/packages/app/src/components/search/SearchModal.tsx @@ -25,6 +25,7 @@ import { Paper, useTheme, } from '@material-ui/core'; +import BuildIcon from '@material-ui/icons/Build'; import LaunchIcon from '@material-ui/icons/Launch'; import { CatalogIcon, @@ -38,6 +39,7 @@ import { catalogApiRef, CATALOG_FILTER_EXISTS, } from '@backstage/plugin-catalog-react'; +import { ToolSearchResultListItem } from '@backstage/plugin-explore'; import { searchPlugin, SearchType } from '@backstage/plugin-search'; import { DefaultResultListItem, @@ -110,6 +112,10 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { value: 'techdocs', name: 'Documentation', }, + { + value: 'tools', + name: 'Tools', + }, ]} /> @@ -209,6 +215,17 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { /> ); break; + case 'tools': + resultItem = ( + } + key={document.location} + result={document} + highlight={highlight} + rank={rank} + /> + ); + break; default: resultItem = ( ({ bar: { @@ -155,6 +157,16 @@ const SearchPage = () => { rank={rank} /> ); + case 'tools': + return ( + } + key={document.location} + result={document} + highlight={highlight} + rank={rank} + /> + ); default: return ( createEnv('permission')); const playlistEnv = useHotMemoize(module, () => createEnv('playlist')); const eventsEnv = useHotMemoize(module, () => createEnv('events')); + const exploreEnv = useHotMemoize(module, () => createEnv('explore')); const eventBasedEntityProviders = await catalogEventBasedProviders( catalogEnv, @@ -172,6 +174,7 @@ async function main() { apiRouter.use('/jenkins', await jenkins(jenkinsEnv)); apiRouter.use('/permission', await permission(permissionEnv)); apiRouter.use('/playlist', await playlist(playlistEnv)); + apiRouter.use('/explore', await explore(exploreEnv)); apiRouter.use(notFoundHandler()); const service = createServiceBuilder(module) diff --git a/packages/backend/src/plugins/explore.ts b/packages/backend/src/plugins/explore.ts new file mode 100644 index 0000000000..6793213dd3 --- /dev/null +++ b/packages/backend/src/plugins/explore.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { + createRouter, + getExampleTools, +} from '@backstage/plugin-explore-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + toolProvider: { + getTools: getExampleTools, + }, + }); +} diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 084b1a54e5..0e6982545f 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -16,6 +16,7 @@ import { useHotCleanup } from '@backstage/backend-common'; import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; +import { ToolDocumentCollatorFactory } from '@backstage/plugin-explore-backend'; import { createRouter } from '@backstage/plugin-search-backend'; import { ElasticSearchSearchEngine } from '@backstage/plugin-search-backend-module-elasticsearch'; import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg'; @@ -84,6 +85,14 @@ export default async function createPlugin( }), }); + indexBuilder.addCollator({ + schedule, + factory: ToolDocumentCollatorFactory.fromConfig(env.config, { + discovery: env.discovery, + logger: env.logger, + }), + }); + // The scheduler controls when documents are gathered from collators and sent // to the search engine for indexing. const { scheduler } = await indexBuilder.build(); diff --git a/plugins/explore-backend/.eslintrc.js b/plugins/explore-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/explore-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/explore-backend/README.md b/plugins/explore-backend/README.md new file mode 100644 index 0000000000..ed065c19a4 --- /dev/null +++ b/plugins/explore-backend/README.md @@ -0,0 +1,93 @@ +# explore-backend + +Welcome to the explore-backend backend plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +### Install the Package + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-explore-backend +``` + +### Create a ToolProvider + +Create a `ToolProvider` to provide `ExploreTool` objects. + +```ts +import { + GetExploreToolsRequest, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; + +export async function getExploreTools( + request: GetExploreToolsRequest, +): Promise { + const { filter } = request ?? {}; + + // Get tool data from a file or some other source + const toolData = getToolData(); + + // Populate ExploreTool[] objects & use filter to filter them + const tools = [...toolData].filter(filterToolPredicate); + + return { + tools, + }; +} +``` + +**NOTE: You can see the `plugins/explore-backend/src/example/getExampleTools.ts` +as a simple hardcoded example.** + +### Adding the plugin to your `packages/backend` + +You'll need to add the plugin to the router in your `backend` package. You can +do this by creating a file called `packages/backend/src/plugins/explore.ts` + +```ts +import { + createRouter, + getExampleTools, +} from '@backstage/plugin-explore-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { getExploreTools } from '../data/explore'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + toolProvider: { + getTools: getExploreTools, + }, + }); +} +``` + +With the `explore.ts` router setup in place, add the router to +`packages/backend/src/index.ts`: + +```diff ++import explore from './plugins/explore'; + +async function main() { + ... + const createEnv = makeCreateEnv(config); + + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); ++ const exploreEnv = useHotMemoize(module, () => createEnv('explore')); + + const apiRouter = Router(); ++ apiRouter.use('/explore', await explore(exploreEnv)); + ... + apiRouter.use(notFoundHandler()); +``` + +### Wire up the Frontend + +See [the explore plugin README](../explore/README.md) for more information. diff --git a/plugins/explore-backend/api-report.md b/plugins/explore-backend/api-report.md new file mode 100644 index 0000000000..7ab37bb598 --- /dev/null +++ b/plugins/explore-backend/api-report.md @@ -0,0 +1,59 @@ +## API Report File for "@backstage/plugin-explore-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { Config } from '@backstage/config'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { ExploreTool } from '@backstage/plugin-explore-common'; +import express from 'express'; +import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; +import { GetExploreToolsResponse } from '@backstage/plugin-explore-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Logger } from 'winston'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Readable } from 'stream'; + +// Warning: (ae-forgotten-export) The symbol "RouterOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export interface ExploreToolProvider { + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-explore-backend" does not have an export "GetExploreToolsRequest" + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-explore-backend" does not have an export "GetExploreToolsResponse" + getTools(request: GetExploreToolsRequest): Promise; +} + +// Warning: (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration +// Warning: (ae-missing-release-tag) "getExampleTools" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function getExampleTools( + request: GetExploreToolsRequest, +): Promise; + +// @public +export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { + // Warning: (ae-forgotten-export) The symbol "ToolDocument" needs to be exported by the entry point index.d.ts + // + // (undocumented) + execute(): AsyncGenerator; + // Warning: (ae-forgotten-export) The symbol "ToolDocumentCollatorFactoryOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static fromConfig( + _config: Config, + options: ToolDocumentCollatorFactoryOptions, + ): ToolDocumentCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; +} +``` diff --git a/plugins/explore-backend/package.json b/plugins/explore-backend/package.json new file mode 100644 index 0000000000..17072e261e --- /dev/null +++ b/plugins/explore-backend/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-explore-backend", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-explore-common": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", + "@types/express": "*", + "express": "^4.18.1", + "express-promise-router": "^4.1.0", + "lodash": "^4.17.21", + "node-fetch": "^2.6.7", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.8", + "msw": "^0.47.0", + "supertest": "^6.2.4" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/explore-backend/src/example/getExampleTools.ts b/plugins/explore-backend/src/example/getExampleTools.ts new file mode 100644 index 0000000000..8d26c8073a --- /dev/null +++ b/plugins/explore-backend/src/example/getExampleTools.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { + GetExploreToolsRequest, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; +import { intersection, isEmpty } from 'lodash'; +import { exampleTools } from './tools'; + +const anyOf = (prop: T | T[], matches: T[]) => + isEmpty(matches) + ? true + : intersection([...[prop]].flat(), matches)?.length > 0; + +/** + * @private Example only - do not use in production + */ +export async function getExampleTools( + request: GetExploreToolsRequest, +): Promise { + const { filter } = request ?? {}; + const tags = filter?.tags ?? []; + const lifecycles = filter?.lifecycle ?? []; + + return { + tools: exampleTools.filter( + t => anyOf(t.tags ?? [], tags) && anyOf(t.lifecycle ?? [], lifecycles), + ), + }; +} diff --git a/plugins/explore-backend/src/example/tools.ts b/plugins/explore-backend/src/example/tools.ts new file mode 100644 index 0000000000..3726e4c5dd --- /dev/null +++ b/plugins/explore-backend/src/example/tools.ts @@ -0,0 +1,98 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { ExploreTool } from '@backstage/plugin-explore-common'; + +/** + * @private Example only - do not use in production + */ +export const exampleTools: ExploreTool[] = [ + { + title: 'New Relic', + description: + 'Observability platform built to help engineers create and monitor their software', + url: '/newrelic', + image: 'https://i.imgur.com/L37ikrX.jpg', + tags: ['newrelic', 'performance', 'monitoring', 'errors', 'alerting'], + }, + { + title: 'CircleCI', + description: + 'Provides builds overview, detailed build info and retriggering functionality for CircleCI.', + url: '/circleci', + image: 'https://miro.medium.com/max/1200/1*hkTBp22vLAqlIHkrkZHPnw.png', + tags: ['circleci', 'ci', 'dev'], + }, + { + title: 'Sentry', + description: + 'Self-hosted and cloud-based error monitoring that helps software teams discover, triage, and prioritize errors in real-time.', + url: '/sentry', + image: 'https://sentry-brand.storage.googleapis.com/sentry-logo-black.png', + tags: ['sentry', 'monitoring', 'errors'], + }, + { + title: 'Lighthouse', + description: + "Google's Lighthouse tool is a great resource for benchmarking and improving the accessibility, performance, SEO, and best practices of your website.", + url: '/lighthouse', + image: + 'https://raw.githubusercontent.com/GoogleChrome/lighthouse/8b3d7f052b2e64dd857e741d7395647f487697e7/assets/lighthouse-logo.png', + tags: ['web', 'seo', 'accessibility', 'performance'], + }, + { + title: 'Tech Radar', + description: + 'Tech Radar is a list of technologies, complemented by an assessment result, called ring assignment.', + url: '/tech-radar', + image: + 'https://storage.googleapis.com/wf-blogs-engineering-media/2018/09/fe13bb32-wf-tech-radar-hero-1024x597.png', + tags: ['standards', 'landscape'], + }, + { + title: 'Cost Insights', + description: 'Insights into cloud costs for your organization.', + url: '/cost-insights', + image: 'https://cloud.google.com/images/press/logo-cloud.png', + tags: ['cloud', 'finops'], + }, + { + title: 'GraphiQL', + description: + 'Integrates GraphiQL as a tool to browse GraphiQL endpoints inside Backstage.', + url: '/graphiql', + image: + 'https://camo.githubusercontent.com/517398c3fbe0687d3d4dcbe05da82970b882e75a/68747470733a2f2f64337676366c703535716a6171632e636c6f756466726f6e742e6e65742f6974656d732f33413061324e314c3346324f304c3377326e316a2f477261706869514c382e706e673f582d436c6f75644170702d56697369746f722d49643d3433363432', + tags: ['graphql', 'dev'], + }, + { + title: 'GitOps Clusters', + description: + 'Create GitOps-managed clusters with Backstage. Currently supports EKS flavors and profiles like Machine Learning Ops (MLOps)', + url: '/gitops-clusters', + image: 'https://miro.medium.com/max/801/1*R28u8gj-hVdDFISoYqPhrQ.png', + tags: ['gitops', 'dev'], + }, + { + title: 'Rollbar', + description: + 'Error monitoring and crash reporting for agile development and continuous delivery', + url: '/rollbar', + image: + 'https://images.ctfassets.net/cj4mgtttlyx7/4DfiWj9CbuHBi10uWK7JHn/5e94a6c5dbd5d50bdcd8d9e78f88689b/rollbar-seo.png', + tags: ['rollbar', 'monitoring', 'errors'], + }, +]; diff --git a/plugins/explore-backend/src/index.ts b/plugins/explore-backend/src/index.ts new file mode 100644 index 0000000000..af9347377d --- /dev/null +++ b/plugins/explore-backend/src/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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. + */ + +/** + * A Backstage plugin for building an exploration page of your software ecosystem + * + * @packageDocumentation + */ + +export * from './search'; +export { createRouter } from './service/router'; +export * from './service/types'; + +/** + * @private Example only - do not use in production + */ +export { getExampleTools } from './example/getExampleTools'; diff --git a/plugins/explore-backend/src/run.ts b/plugins/explore-backend/src/run.ts new file mode 100644 index 0000000000..d945aa13f0 --- /dev/null +++ b/plugins/explore-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts b/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts new file mode 100644 index 0000000000..895eae43be --- /dev/null +++ b/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + IndexableDocument, + DocumentCollatorFactory, +} from '@backstage/plugin-search-common'; +import { Config } from '@backstage/config'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; +import { ExploreTool } from '@backstage/plugin-explore-common'; +import fetch from 'node-fetch'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +/** + * Extended IndexableDocument with explore tool specific properties + * + * @public + */ +export interface ToolDocument extends IndexableDocument, ExploreTool {} + +/** + * The options for the {@link CatalogClient.ToolDocumentCollatorFactory}. + */ +type ToolDocumentCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger; +}; + +/** + * Search collator responsible for collecting explore tools to index. + * + * @public + */ +export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'tools'; + + private readonly discovery: PluginEndpointDiscovery; + private readonly logger: Logger; + + private constructor(options: ToolDocumentCollatorFactoryOptions) { + this.discovery = options.discovery; + this.logger = options.logger; + } + + static fromConfig( + _config: Config, + options: ToolDocumentCollatorFactoryOptions, + ) { + return new ToolDocumentCollatorFactory(options); + } + + async getCollator() { + return Readable.from(this.execute()); + } + + async *execute(): AsyncGenerator { + this.logger.info('Starting collation of explore tools'); + + const tools = await this.fetchTools(); + + for (const tool of tools) { + yield { + ...tool, + text: tool.description, + location: tool.url, + }; + } + + this.logger.info('Finished collation of explore tools'); + } + + private async fetchTools() { + const baseUrl = await this.discovery.getBaseUrl('explore'); + const response = await fetch(`${baseUrl}/tools`); + + if (!response.ok) { + throw new Error( + `Failed to explore fetch tools, ${response.status}: ${response.statusText}`, + ); + } + + const data = await response.json(); + return data.tools; + } +} diff --git a/plugins/explore-backend/src/search/index.ts b/plugins/explore-backend/src/search/index.ts new file mode 100644 index 0000000000..9201518a3d --- /dev/null +++ b/plugins/explore-backend/src/search/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { ToolDocumentCollatorFactory } from './ToolDocumentCollatorFactory'; diff --git a/plugins/explore-backend/src/service/router.test.ts b/plugins/explore-backend/src/service/router.test.ts new file mode 100644 index 0000000000..6f8a7b1110 --- /dev/null +++ b/plugins/explore-backend/src/service/router.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { + ExploreTool, + GetExploreToolsRequest, +} from '@backstage/plugin-explore-common'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; +import { ExploreToolProvider } from './types'; + +const mockTools: ExploreTool[] = [ + { title: 'Tool 1', url: 'https://example.com/tool1', image: '' }, + { title: 'Tool 2', url: 'https://example.com/tool2', image: '' }, + { title: 'Tool 3', url: 'https://example.com/tool2', image: '' }, +]; + +describe('createRouter', () => { + let app: express.Express; + const toolProvider: ExploreToolProvider = { + getTools: async ({}: GetExploreToolsRequest) => { + return { + tools: mockTools, + }; + }, + }; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + toolProvider, + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /tools', () => { + it('returns ok', async () => { + const response = await request(app).get('/tools'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual( + expect.objectContaining({ tools: mockTools }), + ); + }); + }); +}); diff --git a/plugins/explore-backend/src/service/router.ts b/plugins/explore-backend/src/service/router.ts new file mode 100644 index 0000000000..22ffcb54c9 --- /dev/null +++ b/plugins/explore-backend/src/service/router.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { errorHandler } from '@backstage/backend-common'; +import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { ExploreToolProvider } from './types'; + +export interface RouterOptions { + logger: Logger; + toolProvider: ExploreToolProvider; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { toolProvider } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/tools', async (request, response) => { + const requestQuery = parseExploreToolRequestQuery(request.query); + const result = await toolProvider.getTools(requestQuery); + response.json(result); + }); + + router.use(errorHandler()); + + return router; +} + +function parseExploreToolRequestQuery( + params: Record, +): GetExploreToolsRequest { + // TODO: Implement proper query parsing for the filters + // Also consider using a `filter` query string like catalog client does + return { + filter: { + tags: [...[(params?.tag as any) ?? []]].flat(), + lifecycle: [...[(params?.lifecycle as any) ?? []]].flat(), + }, + }; +} diff --git a/plugins/explore-backend/src/service/standaloneServer.ts b/plugins/explore-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..6cf5852f18 --- /dev/null +++ b/plugins/explore-backend/src/service/standaloneServer.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { createServiceBuilder } from '@backstage/backend-common'; +import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ + service: 'explore-backend', + }); + + logger.debug('Starting application server...'); + + const router = await createRouter({ + logger, + toolProvider: { + getTools: async ({}: GetExploreToolsRequest) => { + return { + filter: undefined, + tools: [ + { title: 'Tool 1', url: 'https://example.com/tool1', image: '' }, + { title: 'Tool 2', url: 'https://example.com/tool2', image: '' }, + { title: 'Tool 3', url: 'https://example.com/tool2', image: '' }, + ], + }; + }, + }, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/explore-backend', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/explore-backend/src/service/types.ts b/plugins/explore-backend/src/service/types.ts new file mode 100644 index 0000000000..c2bccaa478 --- /dev/null +++ b/plugins/explore-backend/src/service/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + GetExploreToolsRequest, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; + +/** + * @public + */ +export interface ExploreToolProvider { + /** + * Gets a list of explore tools filtered by the request options. + * + * @param request The request options {@link GetExploreToolsRequest}. + * @returns The response {@link GetExploreToolsResponse}. + */ + getTools(request: GetExploreToolsRequest): Promise; +} diff --git a/plugins/explore-backend/src/setupTests.ts b/plugins/explore-backend/src/setupTests.ts new file mode 100644 index 0000000000..813cdeaae3 --- /dev/null +++ b/plugins/explore-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 {}; diff --git a/plugins/explore-common/.eslintrc.js b/plugins/explore-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/explore-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/explore-common/README.md b/plugins/explore-common/README.md new file mode 100644 index 0000000000..dfd99ae24a --- /dev/null +++ b/plugins/explore-common/README.md @@ -0,0 +1,3 @@ +# explore-common + +Shared types for the `explore*` plugins. diff --git a/plugins/explore-common/api-report.md b/plugins/explore-common/api-report.md new file mode 100644 index 0000000000..79cf80186c --- /dev/null +++ b/plugins/explore-common/api-report.md @@ -0,0 +1,37 @@ +## API Report File for "@backstage/plugin-explore-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public (undocumented) +export type ExploreTool = { + title: string; + description?: string; + url: string; + image: string; + tags?: string[]; + lifecycle?: string; +}; + +// @public (undocumented) +export type ExploreToolFilter = { + tags?: string[]; + lifecycle?: string[]; +}; + +// @public (undocumented) +export type GetExploreToolsRequest = { + filter?: ExploreToolFilter; +}; + +// @public (undocumented) +export type GetExploreToolsResponse = { + tools: ExploreTool[]; +}; + +// Warnings were encountered during analysis: +// +// src/index.d.ts:2:1 - (ae-misplaced-package-tag) The @packageDocumentation comment must appear at the top of entry point *.d.ts file + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/explore-common/package.json b/plugins/explore-common/package.json new file mode 100644 index 0000000000..6365eca8e5 --- /dev/null +++ b/plugins/explore-common/package.json @@ -0,0 +1,39 @@ +{ + "name": "@backstage/plugin-explore-common", + "description": "Common functionalities for the explore plugin", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/explore-common" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/explore-common/src/api.ts b/plugins/explore-common/src/api.ts new file mode 100644 index 0000000000..256671c1af --- /dev/null +++ b/plugins/explore-common/src/api.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { ExploreTool } from './types'; + +/** + * @public + */ +export type ExploreToolFilter = { + /** + * Filter by a list of tags + */ + tags?: string[]; + + /** + * Filter by a list of lifecycle strings + */ + lifecycle?: string[]; +}; + +/** + * @public + */ +export type GetExploreToolsRequest = { + filter?: ExploreToolFilter; +}; + +/** + * @public + */ +export type GetExploreToolsResponse = { + tools: ExploreTool[]; +}; diff --git a/plugins/explore-common/src/index.ts b/plugins/explore-common/src/index.ts new file mode 100644 index 0000000000..b5721d9f57 --- /dev/null +++ b/plugins/explore-common/src/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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. + */ + +/***/ +/** + * Common functionalities for the explore plugin. + * + * @packageDocumentation + */ + +export * from './api'; +export * from './types'; diff --git a/plugins/explore-common/src/types.ts b/plugins/explore-common/src/types.ts new file mode 100644 index 0000000000..25f995f807 --- /dev/null +++ b/plugins/explore-common/src/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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. + */ + +/** + * @public + */ +export type ExploreTool = { + title: string; + description?: string; + url: string; + image: string; + tags?: string[]; + lifecycle?: string; +}; diff --git a/plugins/explore-react/api-report.md b/plugins/explore-react/api-report.md index 8a9d2b8b13..61b3cbee2d 100644 --- a/plugins/explore-react/api-report.md +++ b/plugins/explore-react/api-report.md @@ -4,23 +4,17 @@ ```ts import { ApiRef } from '@backstage/core-plugin-api'; +import * as common from '@backstage/plugin-explore-common'; -// @public (undocumented) -export type ExploreTool = { - title: string; - description?: string; - url: string; - image: string; - tags?: string[]; - lifecycle?: string; -}; +// @public @deprecated (undocumented) +export type ExploreTool = common.ExploreTool; -// @public (undocumented) +// @public @deprecated (undocumented) export interface ExploreToolsConfig { // (undocumented) getTools: () => Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export const exploreToolsConfigRef: ApiRef; ``` diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 879a00daa9..c20679de78 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -32,7 +32,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-plugin-api": "workspace:^" + "@backstage/core-plugin-api": "workspace:^", + "@backstage/plugin-explore-common": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/explore-react/src/tools/api.ts b/plugins/explore-react/src/tools/api.ts index 0fae2ce0fb..32dccfb5dd 100644 --- a/plugins/explore-react/src/tools/api.ts +++ b/plugins/explore-react/src/tools/api.ts @@ -16,22 +16,29 @@ import { createApiRef } from '@backstage/core-plugin-api'; -/** @public */ +/** + * Deprecated types moved to `@backstage/plugin-explore-common` + */ +import * as common from '@backstage/plugin-explore-common'; + +/** + * @deprecated Use ExploreTool from `@backstage/plugin-explore-common` + * @public + */ +export type ExploreTool = common.ExploreTool; + +/** + * @deprecated Use exploreApiRef from `@backstage/plugin-explore` & the `@backstage/plugin-explore-backend` + * @public + */ export const exploreToolsConfigRef = createApiRef({ id: 'plugin.explore.toolsconfig', }); -/** @public */ -export type ExploreTool = { - title: string; - description?: string; - url: string; - image: string; - tags?: string[]; - lifecycle?: string; -}; - -/** @public */ +/** + * @deprecated Use ExploreApi from `@backstage/plugin-explore` & the `@backstage/plugin-explore-backend` + * @public + */ export interface ExploreToolsConfig { getTools: () => Promise; } diff --git a/plugins/explore/package.json b/plugins/explore/package.json index f8388df6e8..cbd8d37932 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -35,8 +35,12 @@ "@backstage/catalog-model": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-explore-common": "workspace:^", "@backstage/plugin-explore-react": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", + "@backstage/plugin-search-react": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/explore/src/api/client.ts b/plugins/explore/src/api/client.ts new file mode 100644 index 0000000000..56f8dd4167 --- /dev/null +++ b/plugins/explore/src/api/client.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { + GetExploreToolsRequest, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; +import { ExploreApi } from './types'; + +/** + * Default implementation of the ExploreApi. + * + * @public + */ +export class ExploreClient implements ExploreApi { + private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; + + constructor({ + discoveryApi, + fetchApi, + }: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + }) { + this.discoveryApi = discoveryApi; + this.fetchApi = fetchApi; + } + + async getTools( + request: GetExploreToolsRequest = {}, + ): Promise { + const { fetch } = this.fetchApi; + + const filter = request.filter ?? {}; + const baseUrl = await this.discoveryApi.getBaseUrl('explore'); + + const tags = filter?.tags?.map(t => `tag=${encodeURIComponent(t)}`) ?? []; + const lifecycles = + filter?.lifecycle?.map(l => `lifecycle=${encodeURIComponent(l)}`) ?? []; + const query = [...tags, ...lifecycles].join('&'); + + const response = await fetch(`${baseUrl}/tools?${query}`); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response.json() as Promise; + } +} diff --git a/plugins/explore/src/api/index.ts b/plugins/explore/src/api/index.ts new file mode 100644 index 0000000000..a7ac3aa8c1 --- /dev/null +++ b/plugins/explore/src/api/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { ExploreClient } from './client'; +export { exploreApiRef } from './types'; +export type { ExploreApi } from './types'; diff --git a/plugins/explore/src/api/types.ts b/plugins/explore/src/api/types.ts new file mode 100644 index 0000000000..0d9eb4ac07 --- /dev/null +++ b/plugins/explore/src/api/types.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { createApiRef } from '@backstage/core-plugin-api'; +import { + GetExploreToolsRequest, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; + +/** + * Interface for the explore plugin. + * + * @public + */ +export interface ExploreApi { + /** + * Returns a list of explore tools. + * + * @param request - The The request query options + */ + getTools(request?: GetExploreToolsRequest): Promise; +} + +export const exploreApiRef = createApiRef({ + id: 'plugin.explore.service', +}); diff --git a/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx b/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx new file mode 100644 index 0000000000..9084f91507 --- /dev/null +++ b/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx @@ -0,0 +1,113 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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, { ReactNode } from 'react'; +import { + Box, + Chip, + Divider, + ListItem, + ListItemIcon, + ListItemText, + makeStyles, +} from '@material-ui/core'; +import { Link } from '@backstage/core-components'; +import { useAnalytics } from '@backstage/core-plugin-api'; +import { + IndexableDocument, + ResultHighlight, +} from '@backstage/plugin-search-common'; +import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; + +const useStyles = makeStyles({ + flexContainer: { + flexWrap: 'wrap', + }, + itemText: { + width: '100%', + wordBreak: 'break-all', + marginBottom: '1rem', + }, +}); + +/** + * Props for {@link ToolSearchResultListItem}. + * + * @public + */ +export interface ToolSearchResultListItemProps { + icon?: ReactNode; + result: IndexableDocument; + highlight?: ResultHighlight; + rank?: number; +} + +/** @public */ +export function ToolSearchResultListItem(props: ToolSearchResultListItemProps) { + const result = props.result as any; + + const classes = useStyles(); + const analytics = useAnalytics(); + const handleClick = () => { + analytics.captureEvent('discover', result.title, { + attributes: { to: result.location }, + value: props.rank, + }); + }; + + return ( + + + {props.icon && {props.icon}} +
+ + ) : ( + result.title + ) + } + secondary={ + props.highlight?.fields.text ? ( + + ) : ( + result.text + ) + } + /> + + {result.tags && + result.tags.map((tag: string) => ( + + ))} + +
+
+ + + ); +} diff --git a/plugins/explore/src/components/ToolSearchResultListItem/index.ts b/plugins/explore/src/components/ToolSearchResultListItem/index.ts new file mode 100644 index 0000000000..1dfdd64b5b --- /dev/null +++ b/plugins/explore/src/components/ToolSearchResultListItem/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 * from './ToolSearchResultListItem'; diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts index f8fba16910..d3af1b99c2 100644 --- a/plugins/explore/src/components/index.ts +++ b/plugins/explore/src/components/index.ts @@ -16,3 +16,4 @@ export * from './DomainCard'; export * from './ExploreLayout'; +export * from './ToolSearchResultListItem'; diff --git a/plugins/explore/src/index.ts b/plugins/explore/src/index.ts index 04f0240968..24591211f7 100644 --- a/plugins/explore/src/index.ts +++ b/plugins/explore/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export * from './api'; export * from './components'; export * from './extensions'; export { explorePlugin, explorePlugin as plugin } from './plugin'; diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts index a4bb538265..7ea9733054 100644 --- a/plugins/explore/src/plugin.ts +++ b/plugins/explore/src/plugin.ts @@ -16,21 +16,43 @@ import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; import { catalogEntityRouteRef, exploreRouteRef } from './routes'; -import { exampleTools } from './util/examples'; -import { createApiFactory, createPlugin } from '@backstage/core-plugin-api'; +import { + createApiFactory, + createPlugin, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; +import { ExploreClient, exploreApiRef } from './api'; /** @public */ export const explorePlugin = createPlugin({ id: 'explore', apis: [ - // Register a default for exploreToolsConfigRef, you may want to override - // the API locally in your app. + createApiFactory({ + api: exploreApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new ExploreClient({ discoveryApi, fetchApi }), + }), + /** + * @deprecated Use ExploreApi from `@backstage/plugin-explore` instead + * + * Register a default for exploreToolsConfigRef, you may want to override + * the API locally in your app. + */ createApiFactory({ api: exploreToolsConfigRef, - deps: {}, - factory: () => ({ + deps: { + exploreApi: exploreApiRef, + }, + factory: ({ exploreApi }) => ({ async getTools() { - return exampleTools; + // TODO: Can we make this backwards compatible so it works off the static frontend example tools? + // return exampleTools; + return (await exploreApi.getTools()).tools; }, }), }), diff --git a/yarn.lock b/yarn.lock index acb2e147e2..fe3feb9581 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5988,6 +5988,36 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-explore-backend@workspace:^, @backstage/plugin-explore-backend@workspace:plugins/explore-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-explore-backend@workspace:plugins/explore-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-explore-common": "workspace:^" + "@backstage/plugin-search-common": "workspace:^" + "@types/express": "*" + "@types/supertest": ^2.0.8 + express: ^4.18.1 + express-promise-router: ^4.1.0 + lodash: ^4.17.21 + msw: ^0.47.0 + node-fetch: ^2.6.7 + supertest: ^6.2.4 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-explore-common@workspace:^, @backstage/plugin-explore-common@workspace:plugins/explore-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-explore-common@workspace:plugins/explore-common" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-explore-react@workspace:^, @backstage/plugin-explore-react@workspace:plugins/explore-react": version: 0.0.0-use.local resolution: "@backstage/plugin-explore-react@workspace:plugins/explore-react" @@ -5995,6 +6025,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-explore-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 @@ -6015,9 +6046,13 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-explore-common": "workspace:^" "@backstage/plugin-explore-react": "workspace:^" + "@backstage/plugin-search-common": "workspace:^" + "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@material-ui/core": ^4.12.2 @@ -21866,6 +21901,8 @@ __metadata: "@backstage/plugin-code-coverage-backend": "workspace:^" "@backstage/plugin-events-backend": "workspace:^" "@backstage/plugin-events-node": "workspace:^" + "@backstage/plugin-explore-backend": "workspace:^" + "@backstage/plugin-explore-common": "workspace:^" "@backstage/plugin-graphql-backend": "workspace:^" "@backstage/plugin-jenkins-backend": "workspace:^" "@backstage/plugin-kafka-backend": "workspace:^" @@ -29054,6 +29091,41 @@ __metadata: languageName: node linkType: hard +"msw@npm:^0.47.0": + version: 0.47.4 + resolution: "msw@npm:0.47.4" + dependencies: + "@mswjs/cookies": ^0.2.2 + "@mswjs/interceptors": ^0.17.5 + "@open-draft/until": ^1.0.3 + "@types/cookie": ^0.4.1 + "@types/js-levenshtein": ^1.1.1 + chalk: 4.1.1 + chokidar: ^3.4.2 + cookie: ^0.4.2 + graphql: ^15.0.0 || ^16.0.0 + headers-polyfill: ^3.1.0 + inquirer: ^8.2.0 + is-node-process: ^1.0.1 + js-levenshtein: ^1.1.6 + node-fetch: ^2.6.7 + outvariant: ^1.3.0 + path-to-regexp: ^6.2.0 + statuses: ^2.0.0 + strict-event-emitter: ^0.2.6 + type-fest: ^2.19.0 + yargs: ^17.3.1 + peerDependencies: + typescript: ">= 4.2.x <= 4.8.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 10ff632641d40384d6622abf4df6399e4ae649db0f676b5d1ee2d0a515ec96f33abe9d4fecba08cdba4b2e43255af419da9eefc020d40a7e10669d0906457197 + languageName: node + linkType: hard + "msw@npm:^0.49.0": version: 0.49.1 resolution: "msw@npm:0.49.1" From 3eaa4bdfbcc44a3f710023414cf76ced4db71bc2 Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Fri, 4 Nov 2022 05:56:56 -0400 Subject: [PATCH 2/9] update api report & intial package versions Signed-off-by: Andrew Thauer --- plugins/explore-backend/api-report.md | 31 +++++++++---------- plugins/explore-backend/package.json | 2 +- plugins/explore-backend/src/index.ts | 5 ++- .../src/search/ToolDocumentCollatorFactory.ts | 20 ++++++------ plugins/explore-backend/src/search/index.ts | 4 +++ plugins/explore-backend/src/service/index.ts | 19 ++++++++++++ plugins/explore-backend/src/service/router.ts | 6 ++++ plugins/explore-backend/src/service/types.ts | 4 +-- plugins/explore-common/README.md | 2 +- plugins/explore-common/api-report.md | 6 ---- plugins/explore-common/package.json | 2 +- plugins/explore-common/src/index.ts | 1 - 12 files changed, 62 insertions(+), 40 deletions(-) create mode 100644 plugins/explore-backend/src/service/index.ts diff --git a/plugins/explore-backend/api-report.md b/plugins/explore-backend/api-report.md index 7ab37bb598..89dd256cd9 100644 --- a/plugins/explore-backend/api-report.md +++ b/plugins/explore-backend/api-report.md @@ -16,36 +16,29 @@ import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Readable } from 'stream'; -// Warning: (ae-forgotten-export) The symbol "RouterOptions" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function createRouter(options: RouterOptions): Promise; // @public (undocumented) export interface ExploreToolProvider { - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-explore-backend" does not have an export "GetExploreToolsRequest" - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-explore-backend" does not have an export "GetExploreToolsResponse" getTools(request: GetExploreToolsRequest): Promise; } -// Warning: (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration -// Warning: (ae-missing-release-tag) "getExampleTools" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + logger: Logger; + // (undocumented) + toolProvider: ExploreToolProvider; +} + // @public -export function getExampleTools( - request: GetExploreToolsRequest, -): Promise; +export interface ToolDocument extends IndexableDocument, ExploreTool {} // @public export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { - // Warning: (ae-forgotten-export) The symbol "ToolDocument" needs to be exported by the entry point index.d.ts - // // (undocumented) execute(): AsyncGenerator; - // Warning: (ae-forgotten-export) The symbol "ToolDocumentCollatorFactoryOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) static fromConfig( _config: Config, @@ -56,4 +49,10 @@ export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { // (undocumented) readonly type: string; } + +// @public +export type ToolDocumentCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger; +}; ``` diff --git a/plugins/explore-backend/package.json b/plugins/explore-backend/package.json index 17072e261e..704eb8c7f5 100644 --- a/plugins/explore-backend/package.json +++ b/plugins/explore-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore-backend", - "version": "0.1.0", + "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore-backend/src/index.ts b/plugins/explore-backend/src/index.ts index af9347377d..ca0daaa8c4 100644 --- a/plugins/explore-backend/src/index.ts +++ b/plugins/explore-backend/src/index.ts @@ -21,10 +21,9 @@ */ export * from './search'; -export { createRouter } from './service/router'; -export * from './service/types'; +export * from './service'; /** - * @private Example only - do not use in production + * @internal Example only - do not use in production */ export { getExampleTools } from './example/getExampleTools'; diff --git a/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts b/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts index 895eae43be..dd9872acec 100644 --- a/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts +++ b/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { - IndexableDocument, - DocumentCollatorFactory, -} from '@backstage/plugin-search-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Config } from '@backstage/config'; +import { ExploreTool } from '@backstage/plugin-explore-common'; +import { + DocumentCollatorFactory, + IndexableDocument, +} from '@backstage/plugin-search-common'; +import fetch from 'node-fetch'; import { Readable } from 'stream'; import { Logger } from 'winston'; -import { ExploreTool } from '@backstage/plugin-explore-common'; -import fetch from 'node-fetch'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; /** * Extended IndexableDocument with explore tool specific properties @@ -33,9 +33,11 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; export interface ToolDocument extends IndexableDocument, ExploreTool {} /** - * The options for the {@link CatalogClient.ToolDocumentCollatorFactory}. + * The options for the {@link ToolDocumentCollatorFactory}. + * + * @public */ -type ToolDocumentCollatorFactoryOptions = { +export type ToolDocumentCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; logger: Logger; }; diff --git a/plugins/explore-backend/src/search/index.ts b/plugins/explore-backend/src/search/index.ts index 9201518a3d..70e9414652 100644 --- a/plugins/explore-backend/src/search/index.ts +++ b/plugins/explore-backend/src/search/index.ts @@ -15,3 +15,7 @@ */ export { ToolDocumentCollatorFactory } from './ToolDocumentCollatorFactory'; +export type { + ToolDocument, + ToolDocumentCollatorFactoryOptions, +} from './ToolDocumentCollatorFactory'; diff --git a/plugins/explore-backend/src/service/index.ts b/plugins/explore-backend/src/service/index.ts new file mode 100644 index 0000000000..376a2bb98c --- /dev/null +++ b/plugins/explore-backend/src/service/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { createRouter } from './router'; +export type { RouterOptions } from './router'; +export * from './types'; diff --git a/plugins/explore-backend/src/service/router.ts b/plugins/explore-backend/src/service/router.ts index 22ffcb54c9..22b5afbc87 100644 --- a/plugins/explore-backend/src/service/router.ts +++ b/plugins/explore-backend/src/service/router.ts @@ -21,11 +21,17 @@ import Router from 'express-promise-router'; import { Logger } from 'winston'; import { ExploreToolProvider } from './types'; +/** + * @public + */ export interface RouterOptions { logger: Logger; toolProvider: ExploreToolProvider; } +/** + * @public + */ export async function createRouter( options: RouterOptions, ): Promise { diff --git a/plugins/explore-backend/src/service/types.ts b/plugins/explore-backend/src/service/types.ts index c2bccaa478..3870924a50 100644 --- a/plugins/explore-backend/src/service/types.ts +++ b/plugins/explore-backend/src/service/types.ts @@ -26,8 +26,8 @@ export interface ExploreToolProvider { /** * Gets a list of explore tools filtered by the request options. * - * @param request The request options {@link GetExploreToolsRequest}. - * @returns The response {@link GetExploreToolsResponse}. + * @param request - The request options {@link @backstage/plugin-explore-common#GetExploreToolsRequest}. + * @returns The response {@link @backstage/plugin-explore-common#GetExploreToolsResponse}. */ getTools(request: GetExploreToolsRequest): Promise; } diff --git a/plugins/explore-common/README.md b/plugins/explore-common/README.md index dfd99ae24a..7e8c835660 100644 --- a/plugins/explore-common/README.md +++ b/plugins/explore-common/README.md @@ -1,3 +1,3 @@ # explore-common -Shared types for the `explore*` plugins. +Shared types for the `explore` plugin. diff --git a/plugins/explore-common/api-report.md b/plugins/explore-common/api-report.md index 79cf80186c..c9a552267c 100644 --- a/plugins/explore-common/api-report.md +++ b/plugins/explore-common/api-report.md @@ -28,10 +28,4 @@ export type GetExploreToolsRequest = { export type GetExploreToolsResponse = { tools: ExploreTool[]; }; - -// Warnings were encountered during analysis: -// -// src/index.d.ts:2:1 - (ae-misplaced-package-tag) The @packageDocumentation comment must appear at the top of entry point *.d.ts file - -// (No @packageDocumentation comment for this package) ``` diff --git a/plugins/explore-common/package.json b/plugins/explore-common/package.json index 6365eca8e5..b1af0db63c 100644 --- a/plugins/explore-common/package.json +++ b/plugins/explore-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore-common", "description": "Common functionalities for the explore plugin", - "version": "0.1.0", + "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore-common/src/index.ts b/plugins/explore-common/src/index.ts index b5721d9f57..50a9b9f649 100644 --- a/plugins/explore-common/src/index.ts +++ b/plugins/explore-common/src/index.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -/***/ /** * Common functionalities for the explore plugin. * From ce34320d919f8467bbdd9a58d85f4a643c690059 Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Fri, 11 Nov 2022 07:14:43 -0500 Subject: [PATCH 3/9] fix api reports + create StaticExploreToolProvider Signed-off-by: Andrew Thauer --- packages/backend/package.json | 1 - packages/backend/src/plugins/explore.ts | 7 +- plugins/explore-backend/api-report.md | 8 ++ .../src/example/{tools.ts => exampleTools.ts} | 0 plugins/explore-backend/src/index.ts | 3 +- plugins/explore-backend/src/service/index.ts | 1 - .../src/service/router.test.ts | 3 +- plugins/explore-backend/src/service/router.ts | 2 +- plugins/explore-backend/src/tools/index.ts | 18 ++++ .../src/tools/providers.test.ts | 85 +++++++++++++++++++ .../getExampleTools.ts => tools/providers.ts} | 41 ++++++--- .../src/{service => tools}/types.ts | 0 plugins/explore/api-report.md | 46 ++++++++++ plugins/explore/src/api/types.ts | 3 + .../ToolExplorerContent.tsx | 5 ++ yarn.lock | 1 - 16 files changed, 200 insertions(+), 24 deletions(-) rename plugins/explore-backend/src/example/{tools.ts => exampleTools.ts} (100%) create mode 100644 plugins/explore-backend/src/tools/index.ts create mode 100644 plugins/explore-backend/src/tools/providers.test.ts rename plugins/explore-backend/src/{example/getExampleTools.ts => tools/providers.ts} (55%) rename plugins/explore-backend/src/{service => tools}/types.ts (100%) diff --git a/packages/backend/package.json b/packages/backend/package.json index 614de918e0..9b376fab67 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -44,7 +44,6 @@ "@backstage/plugin-events-backend": "workspace:^", "@backstage/plugin-events-node": "workspace:^", "@backstage/plugin-explore-backend": "workspace:^", - "@backstage/plugin-explore-common": "workspace:^", "@backstage/plugin-graphql-backend": "workspace:^", "@backstage/plugin-jenkins-backend": "workspace:^", "@backstage/plugin-kafka-backend": "workspace:^", diff --git a/packages/backend/src/plugins/explore.ts b/packages/backend/src/plugins/explore.ts index 6793213dd3..131f9461fa 100644 --- a/packages/backend/src/plugins/explore.ts +++ b/packages/backend/src/plugins/explore.ts @@ -16,7 +16,8 @@ import { createRouter, - getExampleTools, + exampleTools, + StaticExploreToolProvider, } from '@backstage/plugin-explore-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -26,8 +27,6 @@ export default async function createPlugin( ): Promise { return await createRouter({ logger: env.logger, - toolProvider: { - getTools: getExampleTools, - }, + toolProvider: StaticExploreToolProvider.fromData(exampleTools), }); } diff --git a/plugins/explore-backend/api-report.md b/plugins/explore-backend/api-report.md index 89dd256cd9..cc112ed3cc 100644 --- a/plugins/explore-backend/api-report.md +++ b/plugins/explore-backend/api-report.md @@ -32,6 +32,14 @@ export interface RouterOptions { toolProvider: ExploreToolProvider; } +// @public +export class StaticExploreToolProvider implements ExploreToolProvider { + // (undocumented) + static fromData(tools: ExploreTool[]): StaticExploreToolProvider; + // (undocumented) + getTools(request: GetExploreToolsRequest): Promise; +} + // @public export interface ToolDocument extends IndexableDocument, ExploreTool {} diff --git a/plugins/explore-backend/src/example/tools.ts b/plugins/explore-backend/src/example/exampleTools.ts similarity index 100% rename from plugins/explore-backend/src/example/tools.ts rename to plugins/explore-backend/src/example/exampleTools.ts diff --git a/plugins/explore-backend/src/index.ts b/plugins/explore-backend/src/index.ts index ca0daaa8c4..a0e7216621 100644 --- a/plugins/explore-backend/src/index.ts +++ b/plugins/explore-backend/src/index.ts @@ -22,8 +22,9 @@ export * from './search'; export * from './service'; +export * from './tools'; /** * @internal Example only - do not use in production */ -export { getExampleTools } from './example/getExampleTools'; +export { exampleTools } from './example/exampleTools'; diff --git a/plugins/explore-backend/src/service/index.ts b/plugins/explore-backend/src/service/index.ts index 376a2bb98c..8727bd620b 100644 --- a/plugins/explore-backend/src/service/index.ts +++ b/plugins/explore-backend/src/service/index.ts @@ -16,4 +16,3 @@ export { createRouter } from './router'; export type { RouterOptions } from './router'; -export * from './types'; diff --git a/plugins/explore-backend/src/service/router.test.ts b/plugins/explore-backend/src/service/router.test.ts index 6f8a7b1110..5fe5b6c6a3 100644 --- a/plugins/explore-backend/src/service/router.test.ts +++ b/plugins/explore-backend/src/service/router.test.ts @@ -21,9 +21,8 @@ import { } from '@backstage/plugin-explore-common'; import express from 'express'; import request from 'supertest'; - +import { ExploreToolProvider } from '../tools'; import { createRouter } from './router'; -import { ExploreToolProvider } from './types'; const mockTools: ExploreTool[] = [ { title: 'Tool 1', url: 'https://example.com/tool1', image: '' }, diff --git a/plugins/explore-backend/src/service/router.ts b/plugins/explore-backend/src/service/router.ts index 22b5afbc87..393736529a 100644 --- a/plugins/explore-backend/src/service/router.ts +++ b/plugins/explore-backend/src/service/router.ts @@ -19,7 +19,7 @@ import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; -import { ExploreToolProvider } from './types'; +import { ExploreToolProvider } from '../tools'; /** * @public diff --git a/plugins/explore-backend/src/tools/index.ts b/plugins/explore-backend/src/tools/index.ts new file mode 100644 index 0000000000..7c34ce56b8 --- /dev/null +++ b/plugins/explore-backend/src/tools/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { StaticExploreToolProvider } from './providers'; +export type { ExploreToolProvider } from './types'; diff --git a/plugins/explore-backend/src/tools/providers.test.ts b/plugins/explore-backend/src/tools/providers.test.ts new file mode 100644 index 0000000000..7a219e49fd --- /dev/null +++ b/plugins/explore-backend/src/tools/providers.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { ExploreTool } from '@backstage/plugin-explore-common'; +import { StaticExploreToolProvider } from './providers'; + +describe('StaticExploreToolProvider', () => { + const tool1: ExploreTool = { + title: 'Tool 1', + image: 'https://example/image.png', + url: 'https://example.com', + lifecycle: 'production', + tags: ['tag1', 'tag2'], + }; + const tool2: ExploreTool = { + ...tool1, + title: 'Tool 2', + lifecycle: 'production', + tags: ['tag1'], + }; + const tool3: ExploreTool = { + ...tool1, + title: 'Tool 3', + lifecycle: 'experimental', + tags: ['tag2'], + }; + const allTools: ExploreTool[] = [tool1, tool2, tool3]; + + describe('getTools', () => { + it('returns a list of all tools', async () => { + const provider = StaticExploreToolProvider.fromData(allTools); + + await expect(provider.getTools({})).resolves.toEqual({ + tools: allTools, + }); + }); + + it.each([ + [['tag1'], [tool1, tool2]], + [['tag2'], [tool1, tool3]], + [[], allTools], + ])( + 'returns % when filtered by tags %', + async (tagFilter, expectedTools) => { + const provider = StaticExploreToolProvider.fromData(allTools); + + await expect( + provider.getTools({ filter: { tags: tagFilter } }), + ).resolves.toEqual({ + tools: expectedTools, + }); + }, + ); + + it.each([ + [['production'], [tool1, tool2]], + [['experimental'], [tool3]], + [[], allTools], + ])( + 'returns % when filtered by lifecycle %', + async (lifecycleFilter, expectedTools) => { + const provider = StaticExploreToolProvider.fromData(allTools); + + await expect( + provider.getTools({ filter: { lifecycle: lifecycleFilter } }), + ).resolves.toEqual({ + tools: expectedTools, + }); + }, + ); + }); +}); diff --git a/plugins/explore-backend/src/example/getExampleTools.ts b/plugins/explore-backend/src/tools/providers.ts similarity index 55% rename from plugins/explore-backend/src/example/getExampleTools.ts rename to plugins/explore-backend/src/tools/providers.ts index 8d26c8073a..7a87824776 100644 --- a/plugins/explore-backend/src/example/getExampleTools.ts +++ b/plugins/explore-backend/src/tools/providers.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2022 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,12 @@ */ import { + ExploreTool, GetExploreToolsRequest, GetExploreToolsResponse, } from '@backstage/plugin-explore-common'; import { intersection, isEmpty } from 'lodash'; -import { exampleTools } from './tools'; +import { ExploreToolProvider } from './types'; const anyOf = (prop: T | T[], matches: T[]) => isEmpty(matches) @@ -27,18 +28,32 @@ const anyOf = (prop: T | T[], matches: T[]) => : intersection([...[prop]].flat(), matches)?.length > 0; /** - * @private Example only - do not use in production + * A basic ExploreToolProvider implementation using static data. + * + * @public */ -export async function getExampleTools( - request: GetExploreToolsRequest, -): Promise { - const { filter } = request ?? {}; - const tags = filter?.tags ?? []; - const lifecycles = filter?.lifecycle ?? []; +export class StaticExploreToolProvider implements ExploreToolProvider { + private readonly tools: ExploreTool[]; - return { - tools: exampleTools.filter( + static fromData(tools: ExploreTool[]) { + return new StaticExploreToolProvider(tools); + } + + private constructor(tools: ExploreTool[]) { + this.tools = tools; + } + + async getTools( + request: GetExploreToolsRequest, + ): Promise { + const { filter } = request ?? {}; + const tags = filter?.tags ?? []; + const lifecycles = filter?.lifecycle ?? []; + + const tools = this.tools.filter( t => anyOf(t.tags ?? [], tags) && anyOf(t.lifecycle ?? [], lifecycles), - ), - }; + ); + + return { tools }; + } } diff --git a/plugins/explore-backend/src/service/types.ts b/plugins/explore-backend/src/tools/types.ts similarity index 100% rename from plugins/explore-backend/src/service/types.ts rename to plugins/explore-backend/src/tools/types.ts diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index 9653c0fb63..6151f1ea0e 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -5,10 +5,18 @@ ```ts /// +import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; import { DomainEntity } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; +import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; +import { GetExploreToolsResponse } from '@backstage/plugin-explore-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { ReactNode } from 'react'; +import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core'; @@ -30,6 +38,27 @@ export const DomainExplorerContent: (props: { title?: string | undefined; }) => JSX.Element; +// @public +export interface ExploreApi { + getTools(request?: GetExploreToolsRequest): Promise; +} + +// @public (undocumented) +export const exploreApiRef: ApiRef; + +// @public +export class ExploreClient implements ExploreApi { + constructor({ + discoveryApi, + fetchApi, + }: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + }); + // (undocumented) + getTools(request?: GetExploreToolsRequest): Promise; +} + // @public export const ExploreLayout: { (props: ExploreLayoutProps): JSX.Element; @@ -91,4 +120,21 @@ export type SubRoute = { export const ToolExplorerContent: (props: { title?: string | undefined; }) => JSX.Element; + +// @public (undocumented) +export function ToolSearchResultListItem( + props: ToolSearchResultListItemProps, +): JSX.Element; + +// @public +export interface ToolSearchResultListItemProps { + // (undocumented) + highlight?: ResultHighlight; + // (undocumented) + icon?: ReactNode; + // (undocumented) + rank?: number; + // (undocumented) + result: IndexableDocument; +} ``` diff --git a/plugins/explore/src/api/types.ts b/plugins/explore/src/api/types.ts index 0d9eb4ac07..ef9eb646b7 100644 --- a/plugins/explore/src/api/types.ts +++ b/plugins/explore/src/api/types.ts @@ -34,6 +34,9 @@ export interface ExploreApi { getTools(request?: GetExploreToolsRequest): Promise; } +/** + * @public + */ export const exploreApiRef = createApiRef({ id: 'plugin.explore.service', }); diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx index 5404c67393..3e26cab857 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx @@ -28,15 +28,20 @@ import { WarningPanel, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +// import { exploreApiRef } from '../../api'; const Body = () => { const exploreToolsConfigApi = useApi(exploreToolsConfigRef); + // const exploreApi = useApi(exploreApiRef); + const { value: tools, loading, error, } = useAsync(async () => { return await exploreToolsConfigApi.getTools(); + // TODO: Enable the backend ExploreClient + // return (await exploreApi.getTools())?.tools; }, [exploreToolsConfigApi]); if (loading) { diff --git a/yarn.lock b/yarn.lock index fe3feb9581..0f9031d749 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21902,7 +21902,6 @@ __metadata: "@backstage/plugin-events-backend": "workspace:^" "@backstage/plugin-events-node": "workspace:^" "@backstage/plugin-explore-backend": "workspace:^" - "@backstage/plugin-explore-common": "workspace:^" "@backstage/plugin-graphql-backend": "workspace:^" "@backstage/plugin-jenkins-backend": "workspace:^" "@backstage/plugin-kafka-backend": "workspace:^" From 25fe5da05d34ba37aae92a2cc8684712fc196b1c Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Sat, 19 Nov 2022 09:04:42 -0500 Subject: [PATCH 4/9] update explore-backend readme Signed-off-by: Andrew Thauer --- plugins/explore-backend/README.md | 68 ++++++++++++++----------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/plugins/explore-backend/README.md b/plugins/explore-backend/README.md index ed065c19a4..db7d7c7bc6 100644 --- a/plugins/explore-backend/README.md +++ b/plugins/explore-backend/README.md @@ -1,8 +1,9 @@ # explore-backend -Welcome to the explore-backend backend plugin! - -_This plugin was created through the Backstage CLI_ +The `explore-backend` plugin provides a backend service for the Explore plugin. +This allows your organizations tools to be surfaced in the Explore plugin +through an API. It also provides a search collator to make it possible to search +for these tools. ## Getting started @@ -13,36 +14,6 @@ _This plugin was created through the Backstage CLI_ yarn add --cwd packages/backend @backstage/plugin-explore-backend ``` -### Create a ToolProvider - -Create a `ToolProvider` to provide `ExploreTool` objects. - -```ts -import { - GetExploreToolsRequest, - GetExploreToolsResponse, -} from '@backstage/plugin-explore-common'; - -export async function getExploreTools( - request: GetExploreToolsRequest, -): Promise { - const { filter } = request ?? {}; - - // Get tool data from a file or some other source - const toolData = getToolData(); - - // Populate ExploreTool[] objects & use filter to filter them - const tools = [...toolData].filter(filterToolPredicate); - - return { - tools, - }; -} -``` - -**NOTE: You can see the `plugins/explore-backend/src/example/getExampleTools.ts` -as a simple hardcoded example.** - ### Adding the plugin to your `packages/backend` You'll need to add the plugin to the router in your `backend` package. You can @@ -51,20 +22,18 @@ do this by creating a file called `packages/backend/src/plugins/explore.ts` ```ts import { createRouter, - getExampleTools, + StaticExploreToolProvider, } from '@backstage/plugin-explore-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; -import { getExploreTools } from '../data/explore'; +import { exploreTools } from '../path/to/your/exploreTools'; export default async function createPlugin( env: PluginEnvironment, ): Promise { return await createRouter({ logger: env.logger, - toolProvider: { - getTools: getExploreTools, - }, + toolProvider: StaticExploreToolProvider.fromData(exploreTools), }); } ``` @@ -88,6 +57,29 @@ async function main() { apiRouter.use(notFoundHandler()); ``` +### Wire up Search Indexing + +To index explore tools you will need to register the search collator in the +`packages/backend/src/plugins/search.ts` file. + +```diff ++import { ToolDocumentCollatorFactory } from '@backstage/plugin-explore-backend'; + +async function createSearchEngine( + env: PluginEnvironment, +): Promise { + ... ++ indexBuilder.addCollator({ ++ schedule, ++ factory: ToolDocumentCollatorFactory.fromConfig(env.config, { ++ discovery: env.discovery, ++ logger: env.logger, ++ }), ++ }); + ... +} +``` + ### Wire up the Frontend See [the explore plugin README](../explore/README.md) for more information. From ed3e7a1d864bdb1c586e81911ce9623ff5806bc8 Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Sat, 19 Nov 2022 10:03:05 -0500 Subject: [PATCH 5/9] make explore client backwards compatible Signed-off-by: Andrew Thauer --- plugins/explore/api-report.md | 3 +++ plugins/explore/src/api/client.ts | 18 +++++++++++++++ .../ToolExplorerContent.tsx | 12 ++++------ plugins/explore/src/plugin.ts | 22 +++++++++++-------- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index 6151f1ea0e..d184ce01a4 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { DomainEntity } from '@backstage/catalog-model'; +import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; @@ -51,9 +52,11 @@ export class ExploreClient implements ExploreApi { constructor({ discoveryApi, fetchApi, + exploreToolsConfig, }: { discoveryApi: DiscoveryApi; fetchApi: FetchApi; + exploreToolsConfig?: ExploreToolsConfig; }); // (undocumented) getTools(request?: GetExploreToolsRequest): Promise; diff --git a/plugins/explore/src/api/client.ts b/plugins/explore/src/api/client.ts index 56f8dd4167..d51b554879 100644 --- a/plugins/explore/src/api/client.ts +++ b/plugins/explore/src/api/client.ts @@ -20,6 +20,7 @@ import { GetExploreToolsRequest, GetExploreToolsResponse, } from '@backstage/plugin-explore-common'; +import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; import { ExploreApi } from './types'; /** @@ -30,21 +31,38 @@ import { ExploreApi } from './types'; export class ExploreClient implements ExploreApi { private readonly discoveryApi: DiscoveryApi; private readonly fetchApi: FetchApi; + // NOTE: This will be removed in the future as it is replaced by the ExploreApi.getTools method + private readonly exploreToolsConfig: ExploreToolsConfig | undefined; + /** + * @remarks The exploreToolsConfig is for backwards compatibility with the exporeToolsConfigRef + * and will be removed in the future. + */ constructor({ discoveryApi, fetchApi, + exploreToolsConfig = undefined, }: { discoveryApi: DiscoveryApi; fetchApi: FetchApi; + exploreToolsConfig?: ExploreToolsConfig; }) { this.discoveryApi = discoveryApi; this.fetchApi = fetchApi; + this.exploreToolsConfig = exploreToolsConfig; } async getTools( request: GetExploreToolsRequest = {}, ): Promise { + // NOTE: This will be removed in the future as it is replaced by the ExploreApi.getTools method + if (this.exploreToolsConfig) { + const tools = await this.exploreToolsConfig.getTools(); + if (tools) { + return { tools }; + } + } + const { fetch } = this.fetchApi; const filter = request.filter ?? {}; diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx index 3e26cab857..d9cd3a512c 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; import React from 'react'; import useAsync from 'react-use/lib/useAsync'; import { ToolCard } from '../ToolCard'; @@ -28,21 +27,18 @@ import { WarningPanel, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; -// import { exploreApiRef } from '../../api'; +import { exploreApiRef } from '../../api'; const Body = () => { - const exploreToolsConfigApi = useApi(exploreToolsConfigRef); - // const exploreApi = useApi(exploreApiRef); + const exploreApi = useApi(exploreApiRef); const { value: tools, loading, error, } = useAsync(async () => { - return await exploreToolsConfigApi.getTools(); - // TODO: Enable the backend ExploreClient - // return (await exploreApi.getTools())?.tools; - }, [exploreToolsConfigApi]); + return (await exploreApi.getTools())?.tools; + }, [exploreApi]); if (loading) { return ; diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts index 7ea9733054..b6bf7238e3 100644 --- a/plugins/explore/src/plugin.ts +++ b/plugins/explore/src/plugin.ts @@ -23,6 +23,7 @@ import { fetchApiRef, } from '@backstage/core-plugin-api'; import { ExploreClient, exploreApiRef } from './api'; +import { exampleTools } from './util/examples'; /** @public */ export const explorePlugin = createPlugin({ @@ -33,9 +34,11 @@ export const explorePlugin = createPlugin({ deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef, + exploreToolsConfig: exploreToolsConfigRef, }, - factory: ({ discoveryApi, fetchApi }) => - new ExploreClient({ discoveryApi, fetchApi }), + factory: ({ discoveryApi, fetchApi, exploreToolsConfig }) => + // NOTE: The exploreToolsConfig is for backwards compatibility and will be removed in the future + new ExploreClient({ discoveryApi, fetchApi, exploreToolsConfig }), }), /** * @deprecated Use ExploreApi from `@backstage/plugin-explore` instead @@ -45,14 +48,15 @@ export const explorePlugin = createPlugin({ */ createApiFactory({ api: exploreToolsConfigRef, - deps: { - exploreApi: exploreApiRef, - }, - factory: ({ exploreApi }) => ({ + deps: {}, + factory: () => ({ async getTools() { - // TODO: Can we make this backwards compatible so it works off the static frontend example tools? - // return exampleTools; - return (await exploreApi.getTools()).tools; + // TODO: Determine if this should return undefined or exampleTools + // - `undefined` will enable the explore-backend to be used via the ExploreClient backwards compatibility + // which is likely the desired behavior for anyone that has not yet overriden this API + // return undefined as any; + // - `exampleTools` will disable the explore-backend fetching for users that have not yet overriden this API + return exampleTools; }, }), }), From 2b69088c120a9c8017e1b49c3ad335efe34b075d Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Sun, 20 Nov 2022 09:29:36 -0500 Subject: [PATCH 6/9] add explore client tests & rename files Signed-off-by: Andrew Thauer --- plugins/explore-common/src/index.ts | 3 +- plugins/explore-common/src/{ => tools}/api.ts | 0 plugins/explore-common/src/tools/index.ts | 24 ++++ .../explore-common/src/{ => tools}/types.ts | 0 .../src/api/{types.ts => ExploreApi.ts} | 0 plugins/explore/src/api/ExploreClient.test.ts | 114 ++++++++++++++++++ .../src/api/{client.ts => ExploreClient.ts} | 2 +- plugins/explore/src/api/index.ts | 6 +- .../ToolExplorerContent.test.tsx | 16 ++- 9 files changed, 150 insertions(+), 15 deletions(-) rename plugins/explore-common/src/{ => tools}/api.ts (100%) create mode 100644 plugins/explore-common/src/tools/index.ts rename plugins/explore-common/src/{ => tools}/types.ts (100%) rename plugins/explore/src/api/{types.ts => ExploreApi.ts} (100%) create mode 100644 plugins/explore/src/api/ExploreClient.test.ts rename plugins/explore/src/api/{client.ts => ExploreClient.ts} (98%) diff --git a/plugins/explore-common/src/index.ts b/plugins/explore-common/src/index.ts index 50a9b9f649..d829a62e63 100644 --- a/plugins/explore-common/src/index.ts +++ b/plugins/explore-common/src/index.ts @@ -20,5 +20,4 @@ * @packageDocumentation */ -export * from './api'; -export * from './types'; +export * from './tools'; diff --git a/plugins/explore-common/src/api.ts b/plugins/explore-common/src/tools/api.ts similarity index 100% rename from plugins/explore-common/src/api.ts rename to plugins/explore-common/src/tools/api.ts diff --git a/plugins/explore-common/src/tools/index.ts b/plugins/explore-common/src/tools/index.ts new file mode 100644 index 0000000000..50a9b9f649 --- /dev/null +++ b/plugins/explore-common/src/tools/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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. + */ + +/** + * Common functionalities for the explore plugin. + * + * @packageDocumentation + */ + +export * from './api'; +export * from './types'; diff --git a/plugins/explore-common/src/types.ts b/plugins/explore-common/src/tools/types.ts similarity index 100% rename from plugins/explore-common/src/types.ts rename to plugins/explore-common/src/tools/types.ts diff --git a/plugins/explore/src/api/types.ts b/plugins/explore/src/api/ExploreApi.ts similarity index 100% rename from plugins/explore/src/api/types.ts rename to plugins/explore/src/api/ExploreApi.ts diff --git a/plugins/explore/src/api/ExploreClient.test.ts b/plugins/explore/src/api/ExploreClient.test.ts new file mode 100644 index 0000000000..bbec568a09 --- /dev/null +++ b/plugins/explore/src/api/ExploreClient.test.ts @@ -0,0 +1,114 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + ExploreTool, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { ExploreClient } from './ExploreClient'; + +const server = setupServer(); + +describe('ExploreClient', () => { + setupRequestMockHandlers(server); + + const mockBaseUrl = 'http://backstage/api/explore'; + const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; + const fetchApi = new MockFetchApi(); + + let client: ExploreClient; + beforeEach(() => { + client = new ExploreClient({ discoveryApi, fetchApi }); + }); + + describe('getTools', () => { + const mockTools: ExploreTool[] = [ + { + title: 'Tool 1', + image: 'https://example.com/image.png', + url: 'https://example.com', + }, + { + title: 'Tool 2', + image: 'https://example.com/image.png', + url: 'https://example.com', + }, + ]; + + it('should fetch data from the explore-backend', async () => { + const expectedResponse: GetExploreToolsResponse = { + tools: mockTools, + }; + + server.use( + rest.get(`${mockBaseUrl}/tools`, (_, res, ctx) => + res(ctx.json(expectedResponse)), + ), + ); + + const response = await client.getTools(); + expect(response).toEqual(expectedResponse); + }); + + it('should request explore tools with specific filters', async () => { + const expectedResponse: GetExploreToolsResponse = { + tools: mockTools, + }; + + server.use( + rest.get(`${mockBaseUrl}/tools`, (req, res, ctx) => { + expect(req.url.search).toBe('?tag=a&tag=b&lifecycle=alpha'); + return res(ctx.json(expectedResponse)); + }), + ); + + const response = await client.getTools({ + filter: { tags: ['a', 'b'], lifecycle: ['alpha'] }, + }); + expect(response).toEqual(expectedResponse); + }); + }); + + describe('when using exploreToolsConfig for backwards compatibility', () => { + const mockExploreToolsConfig = { + getTools: jest.fn(), + }; + + beforeEach(() => { + client = new ExploreClient({ + discoveryApi, + fetchApi, + exploreToolsConfig: mockExploreToolsConfig, + }); + }); + + it('should return data from the deprecated api', async () => { + mockExploreToolsConfig.getTools.mockResolvedValue([ + { + title: 'Some Tool', + image: 'https://example.com/image.png', + url: 'https://example.com', + }, + ]); + + await client.getTools(); + expect(mockExploreToolsConfig.getTools).toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/explore/src/api/client.ts b/plugins/explore/src/api/ExploreClient.ts similarity index 98% rename from plugins/explore/src/api/client.ts rename to plugins/explore/src/api/ExploreClient.ts index d51b554879..e913aeb041 100644 --- a/plugins/explore/src/api/client.ts +++ b/plugins/explore/src/api/ExploreClient.ts @@ -21,7 +21,7 @@ import { GetExploreToolsResponse, } from '@backstage/plugin-explore-common'; import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; -import { ExploreApi } from './types'; +import { ExploreApi } from './ExploreApi'; /** * Default implementation of the ExploreApi. diff --git a/plugins/explore/src/api/index.ts b/plugins/explore/src/api/index.ts index a7ac3aa8c1..387393f231 100644 --- a/plugins/explore/src/api/index.ts +++ b/plugins/explore/src/api/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { ExploreClient } from './client'; -export { exploreApiRef } from './types'; -export type { ExploreApi } from './types'; +export { ExploreClient } from './ExploreClient'; +export { exploreApiRef } from './ExploreApi'; +export type { ExploreApi } from './ExploreApi'; diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx index da35c9fa39..f3cca5642f 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx @@ -14,25 +14,23 @@ * limitations under the License. */ -import { - ExploreTool, - exploreToolsConfigRef, -} from '@backstage/plugin-explore-react'; +import { ExploreTool } from '@backstage/plugin-explore-common'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { waitFor } from '@testing-library/react'; import React from 'react'; +import { exploreApiRef } from '../../api'; import { ToolExplorerContent } from './ToolExplorerContent'; describe('', () => { - const exploreToolsConfigApi: jest.Mocked = { + const exploreApi: jest.Mocked = { getTools: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} @@ -63,7 +61,7 @@ describe('', () => { tags: ['standards', 'landscape'], }, ]; - exploreToolsConfigApi.getTools.mockResolvedValue(tools); + exploreApi.getTools.mockResolvedValue({ tools }); const { getByText } = await renderInTestApp( @@ -78,7 +76,7 @@ describe('', () => { }); it('renders a custom title', async () => { - exploreToolsConfigApi.getTools.mockResolvedValue([]); + exploreApi.getTools.mockResolvedValue({ tools: [] }); const { getByText } = await renderInTestApp( @@ -90,7 +88,7 @@ describe('', () => { }); it('renders empty state', async () => { - exploreToolsConfigApi.getTools.mockResolvedValue([]); + exploreApi.getTools.mockResolvedValue({ tools: [] }); const { getByText } = await renderInTestApp( From fb9991e9d280e703db6ea7c804a6fef3b018e114 Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Sun, 20 Nov 2022 14:38:10 -0500 Subject: [PATCH 7/9] update explore readmes Signed-off-by: Andrew Thauer --- plugins/explore-backend/README.md | 61 +++++++- plugins/explore-backend/src/tools/index.ts | 4 +- .../StaticExploreToolProvider.test.ts} | 2 +- .../StaticExploreToolProvider.ts} | 2 +- plugins/explore/README.md | 137 +++++++++++------- 5 files changed, 146 insertions(+), 60 deletions(-) rename plugins/explore-backend/src/tools/{providers.test.ts => providers/StaticExploreToolProvider.test.ts} (97%) rename plugins/explore-backend/src/tools/{providers.ts => providers/StaticExploreToolProvider.ts} (97%) diff --git a/plugins/explore-backend/README.md b/plugins/explore-backend/README.md index db7d7c7bc6..927cc47fd7 100644 --- a/plugins/explore-backend/README.md +++ b/plugins/explore-backend/README.md @@ -26,7 +26,18 @@ import { } from '@backstage/plugin-explore-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; -import { exploreTools } from '../path/to/your/exploreTools'; + +// List of tools you want to surface in the Explore plugin "Tools" page. +const tools: ExploreTool[] = [ + { + title: 'New Relic', + description:'new relic plugin', + url: '/newrelic', + image: 'https://i.imgur.com/L37ikrX.jpg', + tags: ['newrelic', 'proxy', 'nerdGraph'], + }, + ... +]; export default async function createPlugin( env: PluginEnvironment, @@ -83,3 +94,51 @@ async function createSearchEngine( ### Wire up the Frontend See [the explore plugin README](../explore/README.md) for more information. + +## Explore Tool Customization + +The `explore-backend` uses the `ExploreToolProvider` interface to provide a list +of tools used in your organization and/or within your Backstage instance. This +can be overriden to provide tools from any source. For example you could create +a `CustomExploreToolProvider` that queries an internal for tools in your +`packages/backend/src/plugins/explore.ts` file. + +```ts +import { + createRouter, + StaticExploreToolProvider, +} from '@backstage/plugin-explore-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +class CustomExploreToolProvider implements ExploreToolProvider { + async getTools( + request: GetExploreToolsRequest, + ): Promise { + const externalTools = await queryExternalTools(request); + + const tools: ExploreTool[] = [ + ...externalTools, + // Backstage Tools + { + title: 'New Relic', + description: 'new relic plugin', + url: '/newrelic', + image: 'https://i.imgur.com/L37ikrX.jpg', + tags: ['newrelic', 'proxy', 'nerdGraph'], + }, + ]; + + return { tools }; + } +} + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + toolProvider: new CustomExploreToolProvider(), + }); +} +``` diff --git a/plugins/explore-backend/src/tools/index.ts b/plugins/explore-backend/src/tools/index.ts index 7c34ce56b8..5e605e0142 100644 --- a/plugins/explore-backend/src/tools/index.ts +++ b/plugins/explore-backend/src/tools/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { StaticExploreToolProvider } from './providers'; -export type { ExploreToolProvider } from './types'; +export { StaticExploreToolProvider } from './providers/StaticExploreToolProvider'; +export * from './types'; diff --git a/plugins/explore-backend/src/tools/providers.test.ts b/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.test.ts similarity index 97% rename from plugins/explore-backend/src/tools/providers.test.ts rename to plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.test.ts index 7a219e49fd..626426eed9 100644 --- a/plugins/explore-backend/src/tools/providers.test.ts +++ b/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.test.ts @@ -15,7 +15,7 @@ */ import { ExploreTool } from '@backstage/plugin-explore-common'; -import { StaticExploreToolProvider } from './providers'; +import { StaticExploreToolProvider } from './StaticExploreToolProvider'; describe('StaticExploreToolProvider', () => { const tool1: ExploreTool = { diff --git a/plugins/explore-backend/src/tools/providers.ts b/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.ts similarity index 97% rename from plugins/explore-backend/src/tools/providers.ts rename to plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.ts index 7a87824776..ad8bcafada 100644 --- a/plugins/explore-backend/src/tools/providers.ts +++ b/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.ts @@ -20,7 +20,7 @@ import { GetExploreToolsResponse, } from '@backstage/plugin-explore-common'; import { intersection, isEmpty } from 'lodash'; -import { ExploreToolProvider } from './types'; +import { ExploreToolProvider } from '../types'; const anyOf = (prop: T | T[], matches: T[]) => isEmpty(matches) diff --git a/plugins/explore/README.md b/plugins/explore/README.md index 94ab0984a0..e8bd4446d9 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -1,42 +1,105 @@ # explore Welcome to the explore plugin! + This plugin helps to visualize the domains and tools in your ecosystem. -## Getting started +## Setup -To install the plugin, add and bind the route in `App.tsx`: +The following sections will help you get the Explore plugin setup and running. -```typescript -import { ExplorePage, explorePlugin } from '@backstage/plugin-explore'; +### Backend -... +You need to setup the +[Explore backend plugin](https://github.com/backstage/backstage/tree/master/plugins/explore-backend) +before you move forward with any of these steps if you haven't already. -bindRoutes({ bind }) { - ... - bind(explorePlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); -}, +### Installation -... +Install this plugin: -} /> +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-explore ``` -And add a link to the sidebar in `Root.tsx`: +### Add the plugin to your `packages/app` -```typescript -import LayersIcon from '@material-ui/icons/Layers'; +Add the root page that the playlist plugin provides to your app. You can choose +any path for the route, but we recommend the following: + +```diff +// packages/app/src/App.tsx ++ import { ExplorePage, explorePlugin } from '@backstage/plugin-explore'; ... - + + } /> + }> + {entityPage} + ++ } /> + ... + +``` + +You may also want to add a link to the playlist page to your application +sidebar: + +```diff +// packages/app/src/components/Root/Root.tsx ++import LayersIcon from '@material-ui/icons/Layers'; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + ++ + ... + +``` + +### Use earch result list item for Explore Tools + +When you have your `packages/app/src/components/search/SearchPage.tsx` file +ready to make modifications, add the following code snippet to add the +`ToolSearchResultListItem` when the type of the search results are +`tool`. + +```diff ++import { ToolSearchResultListItem } from '@backstage/plugin-explore'; + +const SearchPage = () => { + ... + + {({ results }) => ( + + {results.map(({ type, document, highlight, rank }) => { + switch (type) { + ... ++ case 'tools': ++ return ( ++ } ++ key={document.location} ++ result={document} ++ highlight={highlight} ++ rank={rank} ++ /> ++ ); + } + })} + + )} + ... + +... ``` ## Customization -Create a custom explore page in `packages/app/src/components/explore/ExplorePage.tsx`. +Create a custom explore page in +`packages/app/src/components/explore/ExplorePage.tsx`. ```tsx import { @@ -68,7 +131,7 @@ export const explorePage = ; Now register the new explore page in `packages/app/src/App.tsx`. ```diff -+ import { explorePage } from './components/explore/ExplorePage'; ++import { explorePage } from './components/explore/ExplorePage'; const routes = ( @@ -79,39 +142,3 @@ const routes = ( ); ``` - -## ToolExplorer Content Customization - -Override the `exploreToolsConfigRef` API in `/packages/app/src/apis.ts`. - -```tsx -import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; - -export const apis: AnyApiFactory[] = [ - ... - - createApiFactory({ - api: exploreToolsConfigRef, - deps: {}, - factory: () => ({ - async getTools() { - return tools; - }, - /* e.g. tools = [ - { - title: 'New Relic', - description:'new relic plugin, - url: '/newrelic', - image: 'https://i.imgur.com/L37ikrX.jpg', - tags: ['newrelic', 'proxy', 'nerdGraph'], - }, - ] - */ - }), - }), - - .... - -]; - -``` From 8f032ac45000a36ee4bf45096105cee93799707e Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Sun, 20 Nov 2022 15:14:05 -0500 Subject: [PATCH 8/9] update changeset Signed-off-by: Andrew Thauer --- .changeset/rude-sloths-cross.md | 8 +++++++- plugins/explore-backend/README.md | 2 +- plugins/explore/README.md | 2 +- plugins/explore/src/plugin.ts | 6 +++--- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.changeset/rude-sloths-cross.md b/.changeset/rude-sloths-cross.md index 8e22070662..8d3fc486e8 100644 --- a/.changeset/rude-sloths-cross.md +++ b/.changeset/rude-sloths-cross.md @@ -5,4 +5,10 @@ '@backstage/plugin-explore-react': patch --- -Updated to use new @backstage/plugin-explore-backend +Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + +This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + +NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + +BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. diff --git a/plugins/explore-backend/README.md b/plugins/explore-backend/README.md index 927cc47fd7..d1714806f4 100644 --- a/plugins/explore-backend/README.md +++ b/plugins/explore-backend/README.md @@ -99,7 +99,7 @@ See [the explore plugin README](../explore/README.md) for more information. The `explore-backend` uses the `ExploreToolProvider` interface to provide a list of tools used in your organization and/or within your Backstage instance. This -can be overriden to provide tools from any source. For example you could create +can be customized to provide tools from any source. For example you could create a `CustomExploreToolProvider` that queries an internal for tools in your `packages/backend/src/plugins/explore.ts` file. diff --git a/plugins/explore/README.md b/plugins/explore/README.md index e8bd4446d9..0204f34714 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -59,7 +59,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( ``` -### Use earch result list item for Explore Tools +### Use search result list item for Explore Tools When you have your `packages/app/src/components/search/SearchPage.tsx` file ready to make modifications, add the following code snippet to add the diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts index b6bf7238e3..f1869ae849 100644 --- a/plugins/explore/src/plugin.ts +++ b/plugins/explore/src/plugin.ts @@ -23,7 +23,7 @@ import { fetchApiRef, } from '@backstage/core-plugin-api'; import { ExploreClient, exploreApiRef } from './api'; -import { exampleTools } from './util/examples'; +// import { exampleTools } from './util/examples'; /** @public */ export const explorePlugin = createPlugin({ @@ -54,9 +54,9 @@ export const explorePlugin = createPlugin({ // TODO: Determine if this should return undefined or exampleTools // - `undefined` will enable the explore-backend to be used via the ExploreClient backwards compatibility // which is likely the desired behavior for anyone that has not yet overriden this API - // return undefined as any; + return undefined as any; // - `exampleTools` will disable the explore-backend fetching for users that have not yet overriden this API - return exampleTools; + // return exampleTools; }, }), }), From 54ba2b70cf9f0ad4e084ba10a9db268c0275bd09 Mon Sep 17 00:00:00 2001 From: Andrew Thauer Date: Mon, 28 Nov 2022 15:20:47 -0500 Subject: [PATCH 9/9] update comment for explore plugin api usage Signed-off-by: Andrew Thauer --- plugins/explore/src/plugin.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts index f1869ae849..ab8eedf7ef 100644 --- a/plugins/explore/src/plugin.ts +++ b/plugins/explore/src/plugin.ts @@ -51,12 +51,9 @@ export const explorePlugin = createPlugin({ deps: {}, factory: () => ({ async getTools() { - // TODO: Determine if this should return undefined or exampleTools - // - `undefined` will enable the explore-backend to be used via the ExploreClient backwards compatibility - // which is likely the desired behavior for anyone that has not yet overriden this API + // Returning `undefined` will enable the explore-backend to be used via the ExploreClient. + // If this API has been customized and returns data it will be respected first. return undefined as any; - // - `exampleTools` will disable the explore-backend fetching for users that have not yet overriden this API - // return exampleTools; }, }), }),