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

loading...

; + } + + if (error || !value || !value.length) { + return

could not load questions

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