diff --git a/.changeset/four-birds-peel.md b/.changeset/four-birds-peel.md
new file mode 100644
index 0000000000..e90f631858
--- /dev/null
+++ b/.changeset/four-birds-peel.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-home': patch
+---
+
+- Adds new `HomePageStackOverflowQuestions` component which renders a list of stack overflow questions on your homepage.
+
+- Exports `ComponentRenderer` type.
diff --git a/.changeset/mean-rabbits-tell.md b/.changeset/mean-rabbits-tell.md
new file mode 100644
index 0000000000..88d853d9b3
--- /dev/null
+++ b/.changeset/mean-rabbits-tell.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-stack-overflow-backend': minor
+---
+
+Add stack overflow backend plugin
diff --git a/.changeset/witty-years-sniff.md b/.changeset/witty-years-sniff.md
new file mode 100644
index 0000000000..f8d8acf5c1
--- /dev/null
+++ b/.changeset/witty-years-sniff.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-stack-overflow': minor
+---
+
+Add stack overflow plugin
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 44c61f9348..cfe405b7d3 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -44,6 +44,8 @@
/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka
/plugins/search @backstage/reviewers @backstage/techdocs-core
/plugins/search-* @backstage/reviewers @backstage/techdocs-core
+/plugins/stack-overflow @backstage/reviewers @backstage/techdocs-core
+/plugins/stack-overflow-backend @backstage/reviewers @backstage/techdocs-core
/plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers
/plugins/techdocs @backstage/reviewers @backstage/techdocs-core
/plugins/techdocs-* @backstage/reviewers @backstage/techdocs-core
diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md
index 6fae50151c..d5f5c52ea1 100644
--- a/plugins/home/api-report.md
+++ b/plugins/home/api-report.md
@@ -39,6 +39,11 @@ export const ComponentAccordion: ({
ContextProvider?: ((props: any) => JSX.Element) | undefined;
}) => JSX.Element;
+// @public (undocumented)
+export type ComponentRenderer = {
+ Renderer?: (props: RendererProps) => JSX.Element;
+};
+
// Warning: (ae-missing-release-tag) "ComponentTab" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -103,7 +108,6 @@ export const HomepageCompositionRoot: (props: {
children?: ReactNode;
}) => JSX.Element;
-// Warning: (ae-forgotten-export) The symbol "ComponentRenderer" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "HomePageRandomJoke" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -161,5 +165,6 @@ export const WelcomeTitle: () => JSX.Element;
// Warnings were encountered during analysis:
//
-// src/extensions.d.ts:24:5 - (ae-forgotten-export) The symbol "ComponentParts" needs to be exported by the entry point index.d.ts
+// src/extensions.d.ts:6:5 - (ae-forgotten-export) The symbol "RendererProps" needs to be exported by the entry point index.d.ts
+// src/extensions.d.ts:27:5 - (ae-forgotten-export) The symbol "ComponentParts" needs to be exported by the entry point index.d.ts
```
diff --git a/plugins/home/package.json b/plugins/home/package.json
index b787acfdfe..fd83c6ff28 100644
--- a/plugins/home/package.json
+++ b/plugins/home/package.json
@@ -39,7 +39,9 @@
"@backstage/core-plugin-api": "^1.0.0",
"@backstage/plugin-catalog-react": "^1.0.1-next.0",
"@backstage/plugin-search": "^0.7.5-next.0",
+ "@backstage/plugin-stack-overflow": "^0.0.0",
"@backstage/theme": "^0.2.15",
+ "@backstage/config": "^0.1.15",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
diff --git a/plugins/home/src/extensions.tsx b/plugins/home/src/extensions.tsx
index 4a234f9813..4139e301a3 100644
--- a/plugins/home/src/extensions.tsx
+++ b/plugins/home/src/extensions.tsx
@@ -21,6 +21,9 @@ import { InfoCard } from '@backstage/core-components';
import { SettingsModal } from './components';
import { createReactExtension, useApp } from '@backstage/core-plugin-api';
+/**
+ * @public
+ */
export type ComponentRenderer = {
Renderer?: (props: RendererProps) => JSX.Element;
};
diff --git a/plugins/home/src/index.ts b/plugins/home/src/index.ts
index 4871323677..d30b76a7fc 100644
--- a/plugins/home/src/index.ts
+++ b/plugins/home/src/index.ts
@@ -35,3 +35,4 @@ export {
export { SettingsModal, HeaderWorldClock } from './components';
export type { ClockConfig } from './components';
export { createCardExtension } from './extensions';
+export type { ComponentRenderer } from './extensions';
diff --git a/plugins/home/src/templates/DefaultTemplate.stories.tsx b/plugins/home/src/templates/DefaultTemplate.stories.tsx
index c726294ba6..e2e570dcf6 100644
--- a/plugins/home/src/templates/DefaultTemplate.stories.tsx
+++ b/plugins/home/src/templates/DefaultTemplate.stories.tsx
@@ -28,12 +28,15 @@ import {
MockStarredEntitiesApi,
entityRouteRef,
} from '@backstage/plugin-catalog-react';
+import { configApiRef } from '@backstage/core-plugin-api';
+import { ConfigReader } from '@backstage/config';
import {
HomePageSearchBar,
SearchContextProvider,
searchApiRef,
searchPlugin,
} from '@backstage/plugin-search';
+import { HomePageStackOverflowQuestions } from '@backstage/plugin-stack-overflow';
import { Grid, makeStyles } from '@material-ui/core';
import React, { ComponentType } from 'react';
@@ -56,6 +59,14 @@ export default {
starredEntitiesApi,
],
[searchApiRef, { query: () => Promise.resolve({ results: [] }) }],
+ [
+ configApiRef,
+ new ConfigReader({
+ stackoverflow: {
+ baseUrl: 'https://api.stackexchange.com/2.2',
+ },
+ }),
+ ],
]}
>
@@ -131,14 +142,17 @@ export const DefaultTemplate = () => {
{/* placeholder for content */}
-
+
-
- {/* placeholder for content */}
-
-
+
diff --git a/plugins/stack-overflow-backend/.eslintrc.js b/plugins/stack-overflow-backend/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/stack-overflow-backend/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/stack-overflow-backend/README.md b/plugins/stack-overflow-backend/README.md
new file mode 100644
index 0000000000..f2eaf660c2
--- /dev/null
+++ b/plugins/stack-overflow-backend/README.md
@@ -0,0 +1,44 @@
+# Stack Overflow
+
+A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for search) to compose your Backstage App.
+
+## Getting started
+
+Before we begin, make sure:
+
+- You have created your own standalone Backstage app using @backstage/create-app and not using a fork of the backstage repository. If you haven't setup Backstage already, start [here](https://backstage.io/docs/getting-started/).
+
+To use any of the functionality this plugin provides, you need to start by configuring your App with the following config:
+
+```yaml
+stackoverflow:
+ baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance
+```
+
+## Areas of Responsibility
+
+This stack overflow backend plugin is primarily responsible for the following:
+
+- Provides a `StackOverflowQuestionsCollatorFactory`, which can be used in the search backend to index stack overflow questions to your Backstage Search.
+
+### Index Stack Overflow Questions to search
+
+Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started).
+
+When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can modify the `requestParams`.
+
+> Note: if your baseUrl is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions)
+
+```ts
+indexBuilder.addCollator({
+ defaultRefreshIntervalSeconds: 600,
+ factory: StackOverflowQuestionsCollatorFactory.fromConfig(env.config, {
+ logger: env.logger,
+ requestParams: {
+ tagged: ['backstage'],
+ site: 'stackoverflow',
+ pagesize: 100,
+ },
+ }),
+});
+```
diff --git a/plugins/stack-overflow-backend/api-report.md b/plugins/stack-overflow-backend/api-report.md
new file mode 100644
index 0000000000..fc4c610d3e
--- /dev/null
+++ b/plugins/stack-overflow-backend/api-report.md
@@ -0,0 +1,52 @@
+## API Report File for "@backstage/plugin-stack-overflow-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 { IndexableDocument } from '@backstage/plugin-search-common';
+import { Logger } from 'winston';
+import { Readable } from 'stream';
+
+// @public
+export interface StackOverflowDocument extends IndexableDocument {
+ // (undocumented)
+ answers: number;
+ // (undocumented)
+ tags: string[];
+}
+
+// @public
+export class StackOverflowQuestionsCollatorFactory
+ implements DocumentCollatorFactory
+{
+ // (undocumented)
+ execute(): AsyncGenerator;
+ // (undocumented)
+ static fromConfig(
+ config: Config,
+ options: StackOverflowQuestionsCollatorFactoryOptions,
+ ): StackOverflowQuestionsCollatorFactory;
+ // (undocumented)
+ getCollator(): Promise;
+ // (undocumented)
+ protected requestParams: StackOverflowQuestionsRequestParams;
+ // (undocumented)
+ readonly type: string;
+}
+
+// @public
+export type StackOverflowQuestionsCollatorFactoryOptions = {
+ baseUrl?: string;
+ requestParams: StackOverflowQuestionsRequestParams;
+ logger: Logger;
+};
+
+// @public
+export type StackOverflowQuestionsRequestParams = {
+ [key: string]: string | string[] | number;
+};
+```
diff --git a/plugins/stack-overflow-backend/config.d.ts b/plugins/stack-overflow-backend/config.d.ts
new file mode 100644
index 0000000000..7e7211ca33
--- /dev/null
+++ b/plugins/stack-overflow-backend/config.d.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 interface Config {
+ /**
+ * Configuration options for the stack overflow plugin
+ */
+ stackoverflow: {
+ /**
+ * The base url of the Stack Overflow API used for the plugin
+ * @visibility backend
+ */
+ baseUrl: string;
+ };
+}
diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json
new file mode 100644
index 0000000000..bf8c000f44
--- /dev/null
+++ b/plugins/stack-overflow-backend/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@backstage/plugin-stack-overflow-backend",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": false,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/stack-overflow-backend"
+ },
+ "keywords": [
+ "backstage",
+ "stack-overflow"
+ ],
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/plugin-search-common": "^0.3.3-next.0",
+ "@backstage/config": "^1.0.0",
+ "qs": "^6.9.4",
+ "cross-fetch": "^3.1.5",
+ "winston": "^3.2.1"
+ },
+ "files": [
+ "dist",
+ "config"
+ ]
+}
diff --git a/plugins/stack-overflow-backend/src/index.ts b/plugins/stack-overflow-backend/src/index.ts
new file mode 100644
index 0000000000..6e461b8183
--- /dev/null
+++ b/plugins/stack-overflow-backend/src/index.ts
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Stack Overflow backend plugin
+ *
+ * @packageDocumentation
+ */
+
+export * from './search';
diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts
new file mode 100644
index 0000000000..98fc86c2ea
--- /dev/null
+++ b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.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 {
+ IndexableDocument,
+ DocumentCollatorFactory,
+} from '@backstage/plugin-search-common';
+import { Config } from '@backstage/config';
+import { Readable } from 'stream';
+import fetch from 'cross-fetch';
+import qs from 'qs';
+import { Logger } from 'winston';
+
+/**
+ * Extended IndexableDocument with stack overflow specific properties
+ *
+ * @public
+ */
+export interface StackOverflowDocument extends IndexableDocument {
+ answers: number;
+ tags: string[];
+}
+
+/**
+ * Type representing the request parameters accepted by the {@link StackOverflowQuestionsCollatorFactory}
+ *
+ * @public
+ */
+export type StackOverflowQuestionsRequestParams = {
+ [key: string]: string | string[] | number;
+};
+
+/**
+ * Options for {@link StackOverflowQuestionsCollatorFactory}
+ *
+ * @public
+ */
+export type StackOverflowQuestionsCollatorFactoryOptions = {
+ baseUrl?: string;
+ requestParams: StackOverflowQuestionsRequestParams;
+ logger: Logger;
+};
+
+/**
+ * Search collator responsible for collecting stack overflow questions to index.
+ *
+ * @public
+ */
+export class StackOverflowQuestionsCollatorFactory
+ implements DocumentCollatorFactory
+{
+ protected requestParams: StackOverflowQuestionsRequestParams;
+ private readonly baseUrl: string | undefined;
+ private readonly logger: Logger;
+ public readonly type: string = 'stack-overflow';
+
+ private constructor(options: StackOverflowQuestionsCollatorFactoryOptions) {
+ this.baseUrl = options.baseUrl;
+ this.requestParams = options.requestParams;
+ this.logger = options.logger;
+ }
+
+ static fromConfig(
+ config: Config,
+ options: StackOverflowQuestionsCollatorFactoryOptions,
+ ) {
+ const baseUrl =
+ config.getOptionalString('stackoverflow.baseUrl') ||
+ 'https://api.stackexchange.com/2.2';
+ return new StackOverflowQuestionsCollatorFactory({ ...options, baseUrl });
+ }
+
+ async getCollator() {
+ return Readable.from(this.execute());
+ }
+
+ async *execute(): AsyncGenerator {
+ if (!this.baseUrl) {
+ this.logger.debug(
+ `No stackoverflow.baseUrl configured in your app-config.yaml`,
+ );
+ }
+ const params = qs.stringify(this.requestParams, {
+ arrayFormat: 'comma',
+ addQueryPrefix: true,
+ });
+
+ const res = await fetch(`${this.baseUrl}/questions${params}`);
+ const data = await res.json();
+
+ for (const question of data.items) {
+ yield {
+ title: question.title,
+ location: question.link,
+ text: question.owner.display_name,
+ tags: question.tags,
+ answers: question.answer_count,
+ };
+ }
+ }
+}
diff --git a/plugins/stack-overflow-backend/src/search/index.ts b/plugins/stack-overflow-backend/src/search/index.ts
new file mode 100644
index 0000000000..ed3e05cb28
--- /dev/null
+++ b/plugins/stack-overflow-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 * from './StackOverflowQuestionsCollatorFactory';
diff --git a/plugins/stack-overflow/.eslintrc.js b/plugins/stack-overflow/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/stack-overflow/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/stack-overflow/README.md b/plugins/stack-overflow/README.md
new file mode 100644
index 0000000000..6beb843c7d
--- /dev/null
+++ b/plugins/stack-overflow/README.md
@@ -0,0 +1,52 @@
+# Stack Overflow
+
+A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for homepage and search) to compose your Backstage App.
+
+## Getting started
+
+Before we begin, make sure:
+
+- You have created your own standalone Backstage app using @backstage/create-app and not using a fork of the backstage repository. If you haven't setup Backstage already, start [here](https://backstage.io/docs/getting-started/).
+
+To use any of the functionality this plugin provides, you need to start by configuring your App with the following config:
+
+```yaml
+stackoverflow:
+ baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance
+```
+
+## Areas of Responsibility
+
+This stack overflow frontend plugin is primarily responsible for the following:
+
+- Exposing various stack-overflow related components like `` which can be used for composing the search page, and `` which can be used for composing the homepage.
+
+#### Use specific search result list item for Stack Overflow Question
+
+When you have your `packages/app/src/components/search/SearchPage.tsx` file ready to make modifications, add the following code snippet to add the `StackOverflowSearchResultListItem` when the type of the search results are `stack-overflow`.
+
+```tsx
+ case 'stack-overflow':
+ return (
+
+ );
+```
+
+#### Use Stack Overflow Questions on your homepage
+
+Before you are able to add the stack overflow question component to your homepage, you need to go through the [homepage getting started guide](https://backstage.io/docs/getting-started/homepage). When its ready, add the following code snippet to your `packages/app/src/components/home/HomePage.tsx` file.
+
+```tsx
+
+
+
+```
diff --git a/plugins/stack-overflow/api-report.md b/plugins/stack-overflow/api-report.md
new file mode 100644
index 0000000000..277b0272d1
--- /dev/null
+++ b/plugins/stack-overflow/api-report.md
@@ -0,0 +1,46 @@
+## API Report File for "@backstage/plugin-stack-overflow"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackstagePlugin } from '@backstage/core-plugin-api';
+import { ComponentRenderer } from '@backstage/plugin-home';
+import { ReactNode } from 'react';
+
+// @public
+export const HomePageStackOverflowQuestions: (
+ props: ComponentRenderer & {
+ title?: string | undefined;
+ } & StackOverflowQuestionsContentProps,
+) => JSX.Element;
+
+// @public
+export const stackOverflowPlugin: BackstagePlugin<{}, {}>;
+
+// @public
+export type StackOverflowQuestion = {
+ title: string;
+ link: string;
+ owner: Record;
+ tags: string[];
+ answer_count: number;
+};
+
+// @public
+export type StackOverflowQuestionsContentProps = {
+ requestParams: StackOverflowQuestionsRequestParams;
+};
+
+// @public
+export type StackOverflowQuestionsRequestParams = {
+ [key: string]: string | string[] | number;
+};
+
+// @public
+export const StackOverflowSearchResultListItem: (props: {
+ result: any;
+ icon?: ReactNode;
+}) => JSX.Element;
+```
diff --git a/plugins/stack-overflow/config.d.ts b/plugins/stack-overflow/config.d.ts
new file mode 100644
index 0000000000..811893231e
--- /dev/null
+++ b/plugins/stack-overflow/config.d.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 interface Config {
+ /**
+ * Configuration options for the stack overflow plugin
+ */
+ stackoverflow: {
+ /**
+ * The base url of the Stack Overflow API used for the plugin
+ * @visibility frontend
+ */
+ baseUrl: string;
+ };
+}
diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json
new file mode 100644
index 0000000000..9c83ed4856
--- /dev/null
+++ b/plugins/stack-overflow/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@backstage/plugin-stack-overflow",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "frontend-plugin"
+ },
+ "scripts": {
+ "build": "backstage-cli package start",
+ "start": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "diff": "backstage-cli plugin:diff",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/core-components": "^0.9.3-next.0",
+ "@backstage/plugin-home": "^0.4.20-next.0",
+ "@backstage/core-plugin-api": "^1.0.0",
+ "@backstage/plugin-search-common": "^0.3.3-next.0",
+ "@backstage/config": "^1.0.0",
+ "@backstage/theme": "^0.2.15",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "qs": "^6.9.4",
+ "cross-fetch": "^3.1.5",
+ "lodash": "^4.17.21",
+ "react-use": "^17.2.4",
+ "@testing-library/jest-dom": "^5.10.1"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.13.1 || ^17.0.0",
+ "react": "^16.13.1 || ^17.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.16.1-next.0",
+ "@backstage/test-utils": "^1.0.1-next.0",
+ "@testing-library/react": "^12.1.3",
+ "@backstage/core-app-api": "^1.0.1-next.0",
+ "@backstage/dev-utils": "^1.0.1-next.0",
+ "@testing-library/user-event": "^13.1.8",
+ "@types/jest": "^26.0.7",
+ "@types/node": "^14.14.32",
+ "msw": "^0.35.0"
+ },
+ "files": [
+ "dist",
+ "config"
+ ]
+}
diff --git a/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx b/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx
new file mode 100644
index 0000000000..c347399474
--- /dev/null
+++ b/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 { useApi, configApiRef } from '@backstage/core-plugin-api';
+import { Link } from '@backstage/core-components';
+import {
+ IconButton,
+ List,
+ ListItem,
+ ListItemText,
+ ListItemSecondaryAction,
+} from '@material-ui/core';
+import OpenInNewIcon from '@material-ui/icons/OpenInNew';
+import useAsync from 'react-use/lib/useAsync';
+import qs from 'qs';
+import React from 'react';
+import {
+ StackOverflowQuestion,
+ StackOverflowQuestionsContentProps,
+} from '../../types';
+
+/**
+ * A component to display a list of stack overflow questions on the homepage.
+ *
+ * @public
+ */
+
+export const Content = (props: StackOverflowQuestionsContentProps) => {
+ const { requestParams } = props;
+ const configApi = useApi(configApiRef);
+ const baseUrl =
+ configApi.getOptionalString('stackoverflow.baseUrl') ||
+ 'https://api.stackexchange.com/2.2';
+
+ const { value, loading, error } = useAsync(async (): Promise<
+ StackOverflowQuestion[]
+ > => {
+ const params = qs.stringify(requestParams, { addQueryPrefix: true });
+ const response = await fetch(`${baseUrl}/questions${params}`);
+ const data = await response.json();
+ return data.items;
+ }, []);
+
+ if (loading) {
+ return
;
+ }
+
+ const getSecondaryText = (answer_count: Number) =>
+ answer_count > 1 ? `${answer_count} answers` : `${answer_count} answer`;
+
+ return (
+
+ {value.map(question => (
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/plugins/stack-overflow/src/home/StackOverflowQuestions/StackOverflowQuestions.stories.tsx b/plugins/stack-overflow/src/home/StackOverflowQuestions/StackOverflowQuestions.stories.tsx
new file mode 100644
index 0000000000..e19e0f807b
--- /dev/null
+++ b/plugins/stack-overflow/src/home/StackOverflowQuestions/StackOverflowQuestions.stories.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 { HomePageStackOverflowQuestions } from '../../plugin';
+import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
+import { configApiRef } from '@backstage/core-plugin-api';
+import { ConfigReader } from '@backstage/config';
+import { Grid } from '@material-ui/core';
+import React, { ComponentType } from 'react';
+
+export default {
+ title: 'Plugins/Home/Components/StackOverflow',
+ component: HomePageStackOverflowQuestions,
+ decorators: [
+ (Story: ComponentType<{}>) =>
+ wrapInTestApp(
+ <>
+
+
+
+ >,
+ ),
+ ],
+};
+
+export const Default = () => {
+ return (
+
+
+
+ );
+};
diff --git a/plugins/stack-overflow/src/home/StackOverflowQuestions/index.ts b/plugins/stack-overflow/src/home/StackOverflowQuestions/index.ts
new file mode 100644
index 0000000000..1faa9a2426
--- /dev/null
+++ b/plugins/stack-overflow/src/home/StackOverflowQuestions/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 { Content } from './Content';
diff --git a/plugins/stack-overflow/src/index.ts b/plugins/stack-overflow/src/index.ts
new file mode 100644
index 0000000000..264a0d5d7c
--- /dev/null
+++ b/plugins/stack-overflow/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.
+ */
+/**
+ * Stack Overflow frontend plugin
+ *
+ * @packageDocumentation
+ */
+export {
+ stackOverflowPlugin,
+ StackOverflowSearchResultListItem,
+ HomePageStackOverflowQuestions,
+} from './plugin';
+export type {
+ StackOverflowQuestion,
+ StackOverflowQuestionsContentProps,
+ StackOverflowQuestionsRequestParams,
+} from './types';
diff --git a/plugins/stack-overflow/src/plugin.test.ts b/plugins/stack-overflow/src/plugin.test.ts
new file mode 100644
index 0000000000..b25cfe01ee
--- /dev/null
+++ b/plugins/stack-overflow/src/plugin.test.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { stackOverflowPlugin } from './plugin';
+
+describe('stack-overflow', () => {
+ it('should export plugin', () => {
+ expect(stackOverflowPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/stack-overflow/src/plugin.ts b/plugins/stack-overflow/src/plugin.ts
new file mode 100644
index 0000000000..dee48cdf8e
--- /dev/null
+++ b/plugins/stack-overflow/src/plugin.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 {
+ createPlugin,
+ createComponentExtension,
+} from '@backstage/core-plugin-api';
+import { createCardExtension } from '@backstage/plugin-home';
+import { StackOverflowQuestionsContentProps } from './types';
+
+/**
+ * The Backstage plugin that holds stack overflow specific components
+ *
+ * @public
+ */
+export const stackOverflowPlugin = createPlugin({
+ id: 'stack-overflow',
+});
+
+/**
+ * A component to display a stack overflow search result
+ *
+ * @public
+ */
+export const StackOverflowSearchResultListItem = stackOverflowPlugin.provide(
+ createComponentExtension({
+ name: 'StackOverflowResultListItem',
+ component: {
+ lazy: () =>
+ import('./search/StackOverflowSearchResultListItem').then(
+ m => m.StackOverflowSearchResultListItem,
+ ),
+ },
+ }),
+);
+
+/**
+ * A component to display a list of stack overflow questions on the homepage.
+ *
+ * @public
+ */
+export const HomePageStackOverflowQuestions = stackOverflowPlugin.provide(
+ createCardExtension({
+ name: 'HomePageStackOverflowQuestions',
+ title: 'Stack Overflow Questions',
+ components: () => import('./home/StackOverflowQuestions'),
+ }),
+);
diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.stories.tsx b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.stories.tsx
new file mode 100644
index 0000000000..1239cabcbe
--- /dev/null
+++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.stories.tsx
@@ -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 { StackOverflowSearchResultListItem } from '../../plugin';
+import { wrapInTestApp } from '@backstage/test-utils';
+import React, { ComponentType } from 'react';
+
+export default {
+ title: 'Plugins/Search/StackOverflowResultListItem',
+ component: StackOverflowSearchResultListItem,
+ decorators: [(Story: ComponentType<{}>) => wrapInTestApp()],
+};
+
+export const Default = () => {
+ return (
+
+ );
+};
diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.test.tsx b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.test.tsx
new file mode 100644
index 0000000000..61e901591e
--- /dev/null
+++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.test.tsx
@@ -0,0 +1,43 @@
+/*
+ * 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 from 'react';
+import { renderInTestApp } from '@backstage/test-utils';
+import { screen } from '@testing-library/react';
+import { StackOverflowSearchResultListItem } from './StackOverflowSearchResultListItem';
+
+describe('', () => {
+ it('should render without exploding', async () => {
+ await renderInTestApp(
+ ,
+ );
+ expect(
+ screen.getByText(/Customizing Spotify backstage UI/i),
+ ).toBeInTheDocument();
+ expect(screen.getByText(/Tag: backstage/i)).toBeInTheDocument();
+ expect(
+ screen.getByText(/Customizing Spotify backstage UI/i).closest('a'),
+ ).toHaveAttribute('href', 'https://stackoverflow.com/questions/7');
+ });
+});
diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx
new file mode 100644
index 0000000000..c793939593
--- /dev/null
+++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx
@@ -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 React from 'react';
+import _unescape from 'lodash/unescape';
+import { Link } from '@backstage/core-components';
+import {
+ Divider,
+ ListItem,
+ ListItemText,
+ ListItemIcon,
+ Box,
+ Chip,
+} from '@material-ui/core';
+
+type StackOverflowSearchResultListItemProps = {
+ result: any; // TODO(emmaindal): type to StackOverflowDocument.
+ icon?: React.ReactNode;
+};
+
+export const StackOverflowSearchResultListItem = (
+ props: StackOverflowSearchResultListItemProps,
+) => {
+ const { location, title, text, answers, tags } = props.result;
+
+ return (
+
+
+ {props.icon && {props.icon}}
+
+
+
+ {tags &&
+ tags.map((tag: string) => (
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/index.ts b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/index.ts
new file mode 100644
index 0000000000..499f7273aa
--- /dev/null
+++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/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 { StackOverflowSearchResultListItem } from './StackOverflowSearchResultListItem';
diff --git a/plugins/stack-overflow/src/setupTests.ts b/plugins/stack-overflow/src/setupTests.ts
new file mode 100644
index 0000000000..9bb3e72355
--- /dev/null
+++ b/plugins/stack-overflow/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.
+ */
+import '@testing-library/jest-dom';
+import 'cross-fetch/polyfill';
diff --git a/plugins/stack-overflow/src/types.ts b/plugins/stack-overflow/src/types.ts
new file mode 100644
index 0000000000..c3fa08cc4f
--- /dev/null
+++ b/plugins/stack-overflow/src/types.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.
+ */
+
+/**
+ * Type representing a stack overflow question
+ *
+ * @public
+ */
+export type StackOverflowQuestion = {
+ title: string;
+ link: string;
+ owner: Record;
+ tags: string[];
+ answer_count: number;
+};
+
+/**
+ * Props for HomePageStackOverflowQuestions
+ *
+ * @public
+ */
+export type StackOverflowQuestionsContentProps = {
+ requestParams: StackOverflowQuestionsRequestParams;
+};
+
+/**
+ * Type representing the request parameters accepted by the HomePageStackOverflowQuestions component
+ *
+ * @public
+ */
+export type StackOverflowQuestionsRequestParams = {
+ [key: string]: string | string[] | number;
+};
diff --git a/storybook/.storybook/main.js b/storybook/.storybook/main.js
index bd58156d2d..2356e4e41b 100644
--- a/storybook/.storybook/main.js
+++ b/storybook/.storybook/main.js
@@ -9,6 +9,7 @@ const BACKSTAGE_CORE_STORIES = [
'plugins/org',
'plugins/search',
'plugins/home',
+ 'plugins/stack-overflow',
];
module.exports = ({ args }) => {
diff --git a/yarn.lock b/yarn.lock
index 347eb82546..0e6629ecf1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1480,6 +1480,14 @@
lodash "^4.17.21"
uuid "^8.0.0"
+"@backstage/config@^0.1.15":
+ version "0.1.15"
+ resolved "https://registry.npmjs.org/@backstage/config/-/config-0.1.15.tgz#4bad122ad861be5bd61a60639f92d2494fa245c5"
+ integrity sha512-eNJEYYSEu9MkrkBYiMpUBWEc3Bu64YgB9pZZGCMW7/9350tV2wbylEdoBJHslilJlJhiUyTXBckn8Ua7DOH7rw==
+ dependencies:
+ "@backstage/types" "^0.1.3"
+ lodash "^4.17.21"
+
"@backstage/core-components@^0.9.0", "@backstage/core-components@^0.9.2":
version "0.9.2"
resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.9.2.tgz#9a3d79a15039256bbc007e5daa08c983050e0238"
@@ -1580,6 +1588,11 @@
yaml "^1.10.0"
zen-observable "^0.8.15"
+"@backstage/types@^0.1.3":
+ version "0.1.3"
+ resolved "https://registry.npmjs.org/@backstage/types/-/types-0.1.3.tgz#6613d8cbdf97d42d31cd1e66a833df533e7ccf14"
+ integrity sha512-fJVi4oVrlO+G3PRv1fYSll9/X4pE11HLnkI//Geare9sP6wSfp/2zXpLYfKVsG0e24jOl7Swkc8lwLkQ90zMaQ==
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"