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
@@ -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;
};