From 7107d2907c5a66e6eb0534b47550339b2b309c14 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 22 Aug 2022 20:55:16 +0100 Subject: [PATCH 1/6] add stackoverflow api implementation Signed-off-by: Brian Fletcher --- .../src/api/StackOverflowApi.ts | 31 ++++++++ .../src/api/StackOverflowClient.test.ts | 74 +++++++++++++++++++ .../src/api/StackOverflowClient.ts | 46 ++++++++++++ plugins/stack-overflow/src/api/index.ts | 25 +++++++ .../home/StackOverflowQuestions/Content.tsx | 14 +--- plugins/stack-overflow/src/plugin.ts | 15 ++++ 6 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 plugins/stack-overflow/src/api/StackOverflowApi.ts create mode 100644 plugins/stack-overflow/src/api/StackOverflowClient.test.ts create mode 100644 plugins/stack-overflow/src/api/StackOverflowClient.ts create mode 100644 plugins/stack-overflow/src/api/index.ts 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', + }), + }), + ], }); /** From b8190af93923bde8e87e1813ce260e2ba311d626 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 22 Aug 2022 20:58:27 +0100 Subject: [PATCH 2/6] add changeset Signed-off-by: Brian Fletcher --- .changeset/shiny-walls-kiss.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shiny-walls-kiss.md 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. From 5d8f0b0485db4f5b31b93b894b957e077bdaec8d Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 25 Aug 2022 10:04:18 +0100 Subject: [PATCH 3/6] Remove unused qs import Signed-off-by: Brian Fletcher --- plugins/stack-overflow/src/api/StackOverflowClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/stack-overflow/src/api/StackOverflowClient.ts b/plugins/stack-overflow/src/api/StackOverflowClient.ts index bfe51ee448..8e740f77fa 100644 --- a/plugins/stack-overflow/src/api/StackOverflowClient.ts +++ b/plugins/stack-overflow/src/api/StackOverflowClient.ts @@ -20,7 +20,6 @@ import { StackOverflowQuestion, StackOverflowQuestionsRequestParams, } from '../types'; -import qs from 'qs'; export class StackOverflowClient implements StackOverflowApi { private baseUrl: string; From c2ecdf60b06ed040f7f49bf823aa2eafc87eac2c Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 25 Aug 2022 10:32:52 +0100 Subject: [PATCH 4/6] fix qs import Signed-off-by: Brian Fletcher --- plugins/stack-overflow/src/api/StackOverflowClient.ts | 1 + .../stack-overflow/src/home/StackOverflowQuestions/Content.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/stack-overflow/src/api/StackOverflowClient.ts b/plugins/stack-overflow/src/api/StackOverflowClient.ts index 8e740f77fa..ff24b658b0 100644 --- a/plugins/stack-overflow/src/api/StackOverflowClient.ts +++ b/plugins/stack-overflow/src/api/StackOverflowClient.ts @@ -15,6 +15,7 @@ */ import fetch from 'cross-fetch'; +import qs from 'qs'; import { StackOverflowApi } from './StackOverflowApi'; import { StackOverflowQuestion, diff --git a/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx b/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx index c8f5b40d25..a5273ecff9 100644 --- a/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx +++ b/plugins/stack-overflow/src/home/StackOverflowQuestions/Content.tsx @@ -27,7 +27,6 @@ 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, From 83d83eb882607b90ae361fcf9f918d1fb1b5db23 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 30 Aug 2022 14:01:59 +0100 Subject: [PATCH 5/6] add fromConfig to stack overflow api Signed-off-by: Brian Fletcher --- plugins/stack-overflow/src/api/StackOverflowClient.ts | 9 +++++++++ plugins/stack-overflow/src/plugin.ts | 7 +------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/stack-overflow/src/api/StackOverflowClient.ts b/plugins/stack-overflow/src/api/StackOverflowClient.ts index ff24b658b0..609af9dfd4 100644 --- a/plugins/stack-overflow/src/api/StackOverflowClient.ts +++ b/plugins/stack-overflow/src/api/StackOverflowClient.ts @@ -21,6 +21,7 @@ import { StackOverflowQuestion, StackOverflowQuestionsRequestParams, } from '../types'; +import { Config } from '@backstage/config'; export class StackOverflowClient implements StackOverflowApi { private baseUrl: string; @@ -29,6 +30,14 @@ export class StackOverflowClient implements StackOverflowApi { 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 * diff --git a/plugins/stack-overflow/src/plugin.ts b/plugins/stack-overflow/src/plugin.ts index ea52c6844a..a837846708 100644 --- a/plugins/stack-overflow/src/plugin.ts +++ b/plugins/stack-overflow/src/plugin.ts @@ -35,12 +35,7 @@ export const stackOverflowPlugin = createPlugin({ createApiFactory({ api: stackOverflowApiRef, deps: { configApi: configApiRef }, - factory: ({ configApi }) => - new StackOverflowClient({ - baseUrl: - configApi.getOptionalString('stackoverflow.baseUrl') || - 'https://api.stackexchange.com/2.2', - }), + factory: ({ configApi }) => StackOverflowClient.fromConfig(configApi), }), ], }); From 68ff05fbe5fbbdf30e6cc8cf8fda12fcbc54acef Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 30 Aug 2022 15:44:26 +0100 Subject: [PATCH 6/6] make stack overflow api constructor private Signed-off-by: Brian Fletcher --- .../src/api/StackOverflowClient.test.ts | 11 ++++++++--- plugins/stack-overflow/src/api/StackOverflowClient.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/stack-overflow/src/api/StackOverflowClient.test.ts b/plugins/stack-overflow/src/api/StackOverflowClient.test.ts index 78909f9e6d..6574771350 100644 --- a/plugins/stack-overflow/src/api/StackOverflowClient.test.ts +++ b/plugins/stack-overflow/src/api/StackOverflowClient.test.ts @@ -19,6 +19,7 @@ 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(); @@ -61,9 +62,13 @@ describe('StackOverflowClient', () => { it('list questions should return all questions', async () => { setupHandlers(); - const client = new StackOverflowClient({ - baseUrl: 'https://example.com:9191', - }); + const client = StackOverflowClient.fromConfig( + new ConfigReader({ + stackoverflow: { + baseUrl: 'https://example.com:9191', + }, + }), + ); const responseQuestions = await client.listQuestions({ requestParams: { tagged: 'backstage' }, diff --git a/plugins/stack-overflow/src/api/StackOverflowClient.ts b/plugins/stack-overflow/src/api/StackOverflowClient.ts index 609af9dfd4..f4ff5bf190 100644 --- a/plugins/stack-overflow/src/api/StackOverflowClient.ts +++ b/plugins/stack-overflow/src/api/StackOverflowClient.ts @@ -26,7 +26,7 @@ import { Config } from '@backstage/config'; export class StackOverflowClient implements StackOverflowApi { private baseUrl: string; - constructor({ baseUrl }: { baseUrl: string }) { + private constructor({ baseUrl }: { baseUrl: string }) { this.baseUrl = baseUrl; }