Merge pull request #9679 from backstage/emmaindal/stack-overflow-plugin

[Plugin] Stack Overflow
This commit is contained in:
Emma Indal
2022-03-28 11:23:31 +02:00
committed by GitHub
36 changed files with 1076 additions and 7 deletions
+7
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-stack-overflow-backend': minor
---
Add stack overflow backend plugin
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-stack-overflow': minor
---
Add stack overflow plugin
+2
View File
@@ -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
+7 -2
View File
@@ -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
```
+2
View File
@@ -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",
+3
View File
@@ -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;
};
+1
View File
@@ -35,3 +35,4 @@ export {
export { SettingsModal, HeaderWorldClock } from './components';
export type { ClockConfig } from './components';
export { createCardExtension } from './extensions';
export type { ComponentRenderer } from './extensions';
@@ -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',
},
}),
],
]}
>
<Story />
@@ -131,14 +142,17 @@ export const DefaultTemplate = () => {
<Grid item xs={12} md={6}>
<InfoCard title="Composable Section">
{/* placeholder for content */}
<div style={{ height: 210 }} />
<div style={{ height: 370 }} />
</InfoCard>
</Grid>
<Grid item xs={12} md={6}>
<InfoCard title="Composable Section">
{/* placeholder for content */}
<div style={{ height: 210 }} />
</InfoCard>
<HomePageStackOverflowQuestions
requestParams={{
tagged: 'backstage',
site: 'stackoverflow',
pagesize: 5,
}}
/>
</Grid>
</Grid>
</Grid>
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+44
View File
@@ -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,
},
}),
});
```
@@ -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
/// <reference types="node" />
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<StackOverflowDocument>;
// (undocumented)
static fromConfig(
config: Config,
options: StackOverflowQuestionsCollatorFactoryOptions,
): StackOverflowQuestionsCollatorFactory;
// (undocumented)
getCollator(): Promise<Readable>;
// (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;
};
```
+28
View File
@@ -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;
};
}
@@ -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"
]
}
@@ -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';
@@ -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<StackOverflowDocument> {
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,
};
}
}
}
@@ -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';
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+52
View File
@@ -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 `<StackOverflowSearchResultListItem />` which can be used for composing the search page, and `<HomePageStackOverflowQuestions/>` 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 (
<StackOverflowSearchResultListItem
key={document.location}
result={document}
/>
);
```
#### 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
<Grid item xs={12} md={6}>
<HomePageStackOverflowQuestions
requestParams={{
tagged: 'backstage',
site: 'stackoverflow',
pagesize: 5,
}}
/>
</Grid>
```
+46
View File
@@ -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
/// <reference types="react" />
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<string, string>;
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;
```
+28
View File
@@ -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;
};
}
+59
View File
@@ -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"
]
}
@@ -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 <p>loading...</p>;
}
if (error || !value || !value.length) {
return <p>could not load questions</p>;
}
const getSecondaryText = (answer_count: Number) =>
answer_count > 1 ? `${answer_count} answers` : `${answer_count} answer`;
return (
<List>
{value.map(question => (
<ListItem key={question.link}>
<Link to={question.link}>
<ListItemText
primary={question.title}
secondary={getSecondaryText(question.answer_count)}
/>
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="external-link">
<OpenInNewIcon />
</IconButton>
</ListItemSecondaryAction>
</Link>
</ListItem>
))}
</List>
);
};
@@ -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(
<>
<TestApiProvider
apis={[
[
configApiRef,
new ConfigReader({
stackoverflow: {
baseUrl: 'https://api.stackexchange.com/2.2',
},
}),
],
]}
>
<Story />
</TestApiProvider>
</>,
),
],
};
export const Default = () => {
return (
<Grid item xs={12} md={6}>
<HomePageStackOverflowQuestions
requestParams={{
tagged: 'backstage',
site: 'stackoverflow',
pagesize: 5,
}}
/>
</Grid>
);
};
@@ -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';
+30
View File
@@ -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';
+22
View File
@@ -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();
});
});
+61
View File
@@ -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<StackOverflowQuestionsContentProps>({
name: 'HomePageStackOverflowQuestions',
title: 'Stack Overflow Questions',
components: () => import('./home/StackOverflowQuestions'),
}),
);
@@ -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(<Story />)],
};
export const Default = () => {
return (
<StackOverflowSearchResultListItem
result={{
title: 'Customizing Spotify backstage UI',
text: 'Name of Author',
location: 'stackoverflow.question/1',
answers: 0,
tags: ['backstage'],
}}
/>
);
};
@@ -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('<StackOverflowSearchResultListItem/>', () => {
it('should render without exploding', async () => {
await renderInTestApp(
<StackOverflowSearchResultListItem
result={{
title: 'Customizing Spotify backstage UI',
text: 'Name of Author',
location: 'https://stackoverflow.com/questions/7',
answers: 0,
tags: ['backstage'],
}}
/>,
);
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');
});
});
@@ -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 (
<Link to={location}>
<ListItem alignItems="center">
{props.icon && <ListItemIcon>{props.icon}</ListItemIcon>}
<Box flexWrap="wrap">
<ListItemText
primaryTypographyProps={{ variant: 'h6' }}
primary={_unescape(title)}
secondary={`Author: ${text}`}
/>
<Chip label={`Answer(s): ${answers}`} size="small" />
{tags &&
tags.map((tag: string) => (
<Chip key={tag} label={`Tag: ${tag}`} size="small" />
))}
</Box>
</ListItem>
<Divider />
</Link>
);
};
@@ -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';
+17
View File
@@ -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';
+46
View File
@@ -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<string, string>;
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;
};
+1
View File
@@ -9,6 +9,7 @@ const BACKSTAGE_CORE_STORIES = [
'plugins/org',
'plugins/search',
'plugins/home',
'plugins/stack-overflow',
];
module.exports = ({ args }) => {
+13
View File
@@ -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"