diff --git a/plugins/stack-overflow/src/api/StackOverflowApi.ts b/plugins/stack-overflow/src/api/StackOverflowApi.ts new file mode 100644 index 0000000000..6e76f51409 --- /dev/null +++ b/plugins/stack-overflow/src/api/StackOverflowApi.ts @@ -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. + */ + +import { createApiRef } from '@backstage/core-plugin-api'; +import { + StackOverflowQuestion, + StackOverflowQuestionsRequestParams, +} from '../types'; + +export const stackOverflowApiRef = createApiRef({ + id: 'plugin.stackoverflow.service', +}); + +export type StackOverflowApi = { + listQuestions(options?: { + requestParams: StackOverflowQuestionsRequestParams; + }): Promise; +}; diff --git a/plugins/stack-overflow/src/api/StackOverflowClient.test.ts b/plugins/stack-overflow/src/api/StackOverflowClient.test.ts new file mode 100644 index 0000000000..78909f9e6d --- /dev/null +++ b/plugins/stack-overflow/src/api/StackOverflowClient.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2021 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 { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { StackOverflowClient } from './index'; +import { StackOverflowQuestion } from '../types'; + +const server = setupServer(); + +const backstageQuestions: StackOverflowQuestion[] = [ + { + title: 'What is it?', + link: 'https://example.com:9191/questions/1', + tags: ['asdf'], + owner: { who: 'me' }, + answer_count: 3, + }, + { + title: 'Is it?', + link: 'https://example.com:9191/questions/2', + tags: ['asdf'], + owner: { who: 'me' }, + answer_count: 4, + }, +]; + +describe('StackOverflowClient', () => { + setupRequestMockHandlers(server); + + const mockBaseUrl = 'https://example.com:9191'; + + const setupHandlers = () => { + server.use( + rest.get(`${mockBaseUrl}/questions`, (req, res, ctx) => { + return res( + ctx.json({ + items: + req.url.searchParams.get('tagged') === 'backstage' + ? backstageQuestions + : [], + }), + ); + }), + ); + }; + + it('list questions should return all questions', async () => { + setupHandlers(); + const client = new StackOverflowClient({ + baseUrl: 'https://example.com:9191', + }); + + const responseQuestions = await client.listQuestions({ + requestParams: { tagged: 'backstage' }, + }); + expect(responseQuestions.length).toEqual(2); + expect(responseQuestions).toEqual(backstageQuestions); + }); +}); diff --git a/plugins/stack-overflow/src/api/StackOverflowClient.ts b/plugins/stack-overflow/src/api/StackOverflowClient.ts new file mode 100644 index 0000000000..bfe51ee448 --- /dev/null +++ b/plugins/stack-overflow/src/api/StackOverflowClient.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2021 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 fetch from 'cross-fetch'; +import { StackOverflowApi } from './StackOverflowApi'; +import { + StackOverflowQuestion, + StackOverflowQuestionsRequestParams, +} from '../types'; +import qs from 'qs'; + +export class StackOverflowClient implements StackOverflowApi { + private baseUrl: string; + + constructor({ baseUrl }: { baseUrl: string }) { + this.baseUrl = baseUrl; + } + + /** + * List Questions in the StackOverflow instance + * + * */ + async listQuestions(options: { + requestParams: StackOverflowQuestionsRequestParams; + }): Promise { + const params = qs.stringify(options.requestParams, { + addQueryPrefix: true, + }); + const response = await fetch(`${this.baseUrl}/questions${params}`); + const data = await response.json(); + return data.items; + } +} diff --git a/plugins/stack-overflow/src/api/index.ts b/plugins/stack-overflow/src/api/index.ts new file mode 100644 index 0000000000..2d2db5f5cc --- /dev/null +++ b/plugins/stack-overflow/src/api/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * An API definition and client for integrating with the StackOverflow Backend + * API + * + * @packageDocumentation + */ + +export * from './StackOverflowApi'; +export * from './StackOverflowClient'; diff --git a/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx b/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx index fbc3a90a9d..0aaa375f4f 100644 --- a/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx +++ b/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useApi, configApiRef } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; import { Link } from '@backstage/core-components'; import { IconButton, @@ -26,12 +26,12 @@ import { } 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'; +import { stackOverflowApiRef } from '../../api'; /** * A component to display a list of stack overflow questions on the homepage. @@ -41,18 +41,12 @@ import { 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 stackOverflowApi = useApi(stackOverflowApiRef); 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; + return await stackOverflowApi.listQuestions({ requestParams }); }, []); if (loading) { diff --git a/plugins/stack-overflow/src/plugin.ts b/plugins/stack-overflow/src/plugin.ts index dee48cdf8e..ea52c6844a 100644 --- a/plugins/stack-overflow/src/plugin.ts +++ b/plugins/stack-overflow/src/plugin.ts @@ -17,9 +17,12 @@ import { createPlugin, createComponentExtension, + createApiFactory, + configApiRef, } from '@backstage/core-plugin-api'; import { createCardExtension } from '@backstage/plugin-home'; import { StackOverflowQuestionsContentProps } from './types'; +import { stackOverflowApiRef, StackOverflowClient } from './api'; /** * The Backstage plugin that holds stack overflow specific components @@ -28,6 +31,18 @@ import { StackOverflowQuestionsContentProps } from './types'; */ export const stackOverflowPlugin = createPlugin({ id: 'stack-overflow', + apis: [ + createApiFactory({ + api: stackOverflowApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + new StackOverflowClient({ + baseUrl: + configApi.getOptionalString('stackoverflow.baseUrl') || + 'https://api.stackexchange.com/2.2', + }), + }), + ], }); /**