add new stack overflow plugin

Signed-off-by: Emma Indal <emma.indahl@gmail.com>
This commit is contained in:
Emma Indal
2022-02-20 22:19:37 +01:00
parent c87bac58db
commit e4d77a9c54
17 changed files with 691 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+73
View File
@@ -0,0 +1,73 @@
# 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 search plugin is primarily responsible for the following:
- Exposing various stack-overflow related components like `<StackOverflowSearchResultListItem />` which can be used for composing the searchpage, and `<HomePageStackOverflowQuestions/>` which can be used for composing the homepage.
- Provides a `StackOverflowQuestionsCollator`, 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 `StackOverflowQuestionsCollator`. Note that you can modify the request params.
```ts
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 60,
collator: new StackOverflowQuestionsCollator({
config,
requestParams: {
tagged: ['backstage'],
site: 'stackoverflow',
pagesize: 100,
},
}),
});
```
#### 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={result.document.location}
result={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>
```
+56
View File
@@ -0,0 +1,56 @@
{
"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"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core-components": "^0.8.8",
"@backstage/plugin-home": "^0.4.15",
"@backstage/core-plugin-api": "^0.6.0",
"@backstage/search-common": "^0.2.2",
"@backstage/config": "^0.1.13",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"qs": "^6.10.3",
"cross-fetch": "^3.0.6",
"lodash": "^4.17.21",
"react-use": "^17.2.4",
"@testing-library/jest-dom": "^5.10.1"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.13.2-next.0",
"@backstage/core-app-api": "^0.5.2",
"@backstage/dev-utils": "^0.2.21-next.0",
"@backstage/test-utils": "^0.2.4",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "*",
"@types/node": "*",
"msw": "^0.35.0",
"cross-fetch": "^3.0.6"
},
"files": [
"dist"
]
}
@@ -0,0 +1,85 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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';
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');
const { value, loading, error } = useAsync(async (): Promise<
StackOverflowQuestion[]
> => {
const params = requestParams ? `?${qs.stringify(requestParams)}` : '';
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';
+21
View File
@@ -0,0 +1,21 @@
/*
* 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 {
stackOverflowPlugin,
StackOverflowSearchResultListItem,
HomePageStackOverflowQuestions,
} from './plugin';
export { StackOverflowQuestionsCollator } from './search/StackOverflowQuestionsCollator';
+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();
});
});
+56
View File
@@ -0,0 +1,56 @@
/*
* 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';
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,72 @@
/*
* 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, DocumentCollator } from '@backstage/search-common';
import { Config } from '@backstage/config';
import fetch from 'cross-fetch';
import qs from 'qs';
import {
StackOverflowQuestion,
StackOverflowQuestionsRequestParams,
} from '../../types';
interface StackOverflowDocument extends IndexableDocument {
answers: number;
tags: string[];
}
export class StackOverflowQuestionsCollator implements DocumentCollator {
protected baseUrl: string;
protected requestParams: StackOverflowQuestionsRequestParams;
public readonly type: string = 'stack-overflow';
constructor({
config,
requestParams,
}: {
config: Config;
requestParams: StackOverflowQuestionsRequestParams;
}) {
this.baseUrl = config.getString('stackoverflow.baseUrl');
this.requestParams = requestParams;
}
async execute() {
const params = this.requestParams
? `?${qs.stringify(this.requestParams)}`
: '';
const res = await fetch(`${this.baseUrl}/questions${params}`);
const data = await res.json();
return data.items.map(
({
title,
link,
owner: { display_name },
tags,
answer_count,
}: StackOverflowQuestion): StackOverflowDocument => {
return {
title: title,
location: link,
text: display_name,
tags: tags,
answers: 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 { StackOverflowQuestionsCollator } from './StackOverflowQuestionsCollator';
@@ -0,0 +1,40 @@
/*
* 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';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
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} target="_blank">
<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';
+31
View File
@@ -0,0 +1,31 @@
/*
* 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 type StackOverflowQuestion = {
title: string;
link: string;
owner: Record<string, string>;
tags: string[];
answer_count: number;
};
export type StackOverflowQuestionsContentProps = {
requestParams: StackOverflowQuestionsRequestParams;
};
export type StackOverflowQuestionsRequestParams = {
[key: string]: string | string[] | number;
};