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}}
+