diff --git a/.changeset/shiny-walls-kiss.md b/.changeset/shiny-walls-kiss.md new file mode 100644 index 0000000000..0f13acb154 --- /dev/null +++ b/.changeset/shiny-walls-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stack-overflow': patch +--- + +Create a front end API. 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..6574771350 --- /dev/null +++ b/plugins/stack-overflow/src/api/StackOverflowClient.test.ts @@ -0,0 +1,79 @@ +/* + * 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'; +import { ConfigReader } from '@backstage/config'; + +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 = StackOverflowClient.fromConfig( + new ConfigReader({ + stackoverflow: { + 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..f4ff5bf190 --- /dev/null +++ b/plugins/stack-overflow/src/api/StackOverflowClient.ts @@ -0,0 +1,55 @@ +/* + * 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 qs from 'qs'; +import { StackOverflowApi } from './StackOverflowApi'; +import { + StackOverflowQuestion, + StackOverflowQuestionsRequestParams, +} from '../types'; +import { Config } from '@backstage/config'; + +export class StackOverflowClient implements StackOverflowApi { + private baseUrl: string; + + private constructor({ baseUrl }: { baseUrl: string }) { + this.baseUrl = baseUrl; + } + + static fromConfig(config: Config) { + return new StackOverflowClient({ + baseUrl: + config.getOptionalString('stackoverflow.baseUrl') || + 'https://api.stackexchange.com/2.2', + }); + } + + /** + * 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 d562b749b1..a5273ecff9 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, @@ -27,12 +27,12 @@ import { import OpenInNewIcon from '@material-ui/icons/OpenInNew'; import useAsync from 'react-use/lib/useAsync'; import _unescape from 'lodash/unescape'; -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. @@ -42,18 +42,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..a837846708 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,13 @@ import { StackOverflowQuestionsContentProps } from './types'; */ export const stackOverflowPlugin = createPlugin({ id: 'stack-overflow', + apis: [ + createApiFactory({ + api: stackOverflowApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => StackOverflowClient.fromConfig(configApi), + }), + ], }); /**