add new stack overflow plugin
Signed-off-by: Emma Indal <emma.indahl@gmail.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
+62
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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'),
|
||||
}),
|
||||
);
|
||||
+72
@@ -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';
|
||||
+40
@@ -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'],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+43
@@ -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');
|
||||
});
|
||||
});
|
||||
+59
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user