From fbf10445af8f3ea99277484a82264ad2ab7e47b2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 19:50:49 +0100 Subject: [PATCH 01/42] plugins: added todo frontend and backend plugin Signed-off-by: Patrik Oldsberg --- .github/styles/vocab.txt | 1 + plugins/todo-backend/.eslintrc.js | 3 + plugins/todo-backend/README.md | 13 +++ plugins/todo-backend/package.json | 40 +++++++ plugins/todo-backend/src/index.ts | 17 +++ plugins/todo-backend/src/run.ts | 33 ++++++ .../todo-backend/src/service/router.test.ts | 45 ++++++++ plugins/todo-backend/src/service/router.ts | 40 +++++++ .../src/service/standaloneServer.ts | 62 +++++++++++ plugins/todo-backend/src/setupTests.ts | 17 +++ plugins/todo/.eslintrc.js | 3 + plugins/todo/README.md | 13 +++ plugins/todo/dev/index.tsx | 26 +++++ plugins/todo/package.json | 47 ++++++++ .../ExampleComponent.test.tsx | 44 ++++++++ .../ExampleComponent/ExampleComponent.tsx | 53 +++++++++ .../src/components/ExampleComponent/index.ts | 16 +++ .../ExampleFetchComponent.test.tsx | 40 +++++++ .../ExampleFetchComponent.tsx | 105 ++++++++++++++++++ .../components/ExampleFetchComponent/index.ts | 16 +++ plugins/todo/src/index.ts | 16 +++ plugins/todo/src/plugin.test.ts | 22 ++++ plugins/todo/src/plugin.ts | 33 ++++++ plugins/todo/src/routes.ts | 20 ++++ plugins/todo/src/setupTests.ts | 17 +++ 25 files changed, 742 insertions(+) create mode 100644 plugins/todo-backend/.eslintrc.js create mode 100644 plugins/todo-backend/README.md create mode 100644 plugins/todo-backend/package.json create mode 100644 plugins/todo-backend/src/index.ts create mode 100644 plugins/todo-backend/src/run.ts create mode 100644 plugins/todo-backend/src/service/router.test.ts create mode 100644 plugins/todo-backend/src/service/router.ts create mode 100644 plugins/todo-backend/src/service/standaloneServer.ts create mode 100644 plugins/todo-backend/src/setupTests.ts create mode 100644 plugins/todo/.eslintrc.js create mode 100644 plugins/todo/README.md create mode 100644 plugins/todo/dev/index.tsx create mode 100644 plugins/todo/package.json create mode 100644 plugins/todo/src/components/ExampleComponent/ExampleComponent.test.tsx create mode 100644 plugins/todo/src/components/ExampleComponent/ExampleComponent.tsx create mode 100644 plugins/todo/src/components/ExampleComponent/index.ts create mode 100644 plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx create mode 100644 plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx create mode 100644 plugins/todo/src/components/ExampleFetchComponent/index.ts create mode 100644 plugins/todo/src/index.ts create mode 100644 plugins/todo/src/plugin.test.ts create mode 100644 plugins/todo/src/plugin.ts create mode 100644 plugins/todo/src/routes.ts create mode 100644 plugins/todo/src/setupTests.ts diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 8997360782..489727ca4d 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -88,6 +88,7 @@ Telenor Templater Templaters Thauer +todo Tolerations Tuite Trendyol diff --git a/plugins/todo-backend/.eslintrc.js b/plugins/todo-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/todo-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/todo-backend/README.md b/plugins/todo-backend/README.md new file mode 100644 index 0000000000..78da07c683 --- /dev/null +++ b/plugins/todo-backend/README.md @@ -0,0 +1,13 @@ +# todo + +Welcome to the todo backend plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/todo](http://localhost:3000/todo). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json new file mode 100644 index 0000000000..47c0202bba --- /dev/null +++ b/plugins/todo-backend/package.json @@ -0,0 +1,40 @@ +{ + "name": "@backstage/plugin-todo-backend", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.5.5", + "@backstage/config": "^0.1.3", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "winston": "^3.2.1", + "cross-fetch": "^3.0.6", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.6.3", + "@types/supertest": "^2.0.8", + "supertest": "^4.0.2", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/todo-backend/src/index.ts b/plugins/todo-backend/src/index.ts new file mode 100644 index 0000000000..7612c392a2 --- /dev/null +++ b/plugins/todo-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 * from './service/router'; diff --git a/plugins/todo-backend/src/run.ts b/plugins/todo-backend/src/run.ts new file mode 100644 index 0000000000..b96989e4b8 --- /dev/null +++ b/plugins/todo-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/todo-backend/src/service/router.test.ts b/plugins/todo-backend/src/service/router.test.ts new file mode 100644 index 0000000000..0aaeafa379 --- /dev/null +++ b/plugins/todo-backend/src/service/router.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts new file mode 100644 index 0000000000..3ea8219365 --- /dev/null +++ b/plugins/todo-backend/src/service/router.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { errorHandler } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.send({ status: 'ok' }); + }); + router.use(errorHandler()); + return router; +} diff --git a/plugins/todo-backend/src/service/standaloneServer.ts b/plugins/todo-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..4901ec1895 --- /dev/null +++ b/plugins/todo-backend/src/service/standaloneServer.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2021 Spotify AB + * + * 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. + */ +/* + * Copyright 2020 Spotify AB + * + * 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 { createServiceBuilder } from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'todo-backend' }); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + }); + + const service = createServiceBuilder(module) + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/todo', router); + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/todo-backend/src/setupTests.ts b/plugins/todo-backend/src/setupTests.ts new file mode 100644 index 0000000000..ba33cf996b --- /dev/null +++ b/plugins/todo-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 {}; diff --git a/plugins/todo/.eslintrc.js b/plugins/todo/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/todo/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/todo/README.md b/plugins/todo/README.md new file mode 100644 index 0000000000..a74a5c351b --- /dev/null +++ b/plugins/todo/README.md @@ -0,0 +1,13 @@ +# todo + +Welcome to the todo plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/todo](http://localhost:3000/todo). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx new file mode 100644 index 0000000000..3177a51d78 --- /dev/null +++ b/plugins/todo/dev/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { createDevApp } from '@backstage/dev-utils'; +import { todoPlugin, TodoPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(todoPlugin) + .addPage({ + element: , + title: 'Root Page', + }) + .render(); diff --git a/plugins/todo/package.json b/plugins/todo/package.json new file mode 100644 index 0000000000..c587fb6dc1 --- /dev/null +++ b/plugins/todo/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/plugin-todo", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core": "^0.7.0", + "@backstage/theme": "^0.2.3", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.6.3", + "@backstage/dev-utils": "^0.1.13", + "@backstage/test-utils": "^0.1.8", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "msw": "^0.21.2", + "cross-fetch": "^3.0.6" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/todo/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/todo/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 0000000000..555f3be75d --- /dev/null +++ b/plugins/todo/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { ExampleComponent } from './ExampleComponent'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { msw, renderInTestApp } from '@backstage/test-utils'; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + msw.setupDefaultHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('Welcome to todo!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/todo/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/todo/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 0000000000..3dc0345ee4 --- /dev/null +++ b/plugins/todo/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core'; +import { ExampleFetchComponent } from '../ExampleFetchComponent'; + +export const ExampleComponent = () => ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + + + + + + + +
+); diff --git a/plugins/todo/src/components/ExampleComponent/index.ts b/plugins/todo/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..337d24d5c5 --- /dev/null +++ b/plugins/todo/src/components/ExampleComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { ExampleComponent } from './ExampleComponent'; diff --git a/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx new file mode 100644 index 0000000000..6a5c0351d6 --- /dev/null +++ b/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { render } from '@testing-library/react'; +import { ExampleFetchComponent } from './ExampleFetchComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { msw } from '@backstage/test-utils'; + +describe('ExampleFetchComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + msw.setupDefaultHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('https://randomuser.me/*', (_, res, ctx) => + res(ctx.status(200), ctx.delay(2000), ctx.json({})), + ), + ); + }); + it('should render', async () => { + const rendered = render(); + expect(await rendered.findByTestId('progress')).toBeInTheDocument(); + }); +}); diff --git a/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx new file mode 100644 index 0000000000..1390c8950f --- /dev/null +++ b/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { makeStyles } from '@material-ui/core/styles'; +import { Table, TableColumn, Progress } from '@backstage/core'; +import Alert from '@material-ui/lab/Alert'; +import { useAsync } from 'react-use'; + +const useStyles = makeStyles({ + avatar: { + height: 32, + width: 32, + borderRadius: '50%', + }, +}); + +type User = { + gender: string; // "male" + name: { + title: string; // "Mr", + first: string; // "Duane", + last: string; // "Reed" + }; + location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…} + email: string; // "duane.reed@example.com" + login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…} + dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37} + registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14} + phone: string; // "07-2154-5651" + cell: string; // "0405-592-879" + id: { + name: string; // "TFN", + value: string; // "796260432" + }; + picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…} + nat: string; // "AU" +}; + +type DenseTableProps = { + users: User[]; +}; + +export const DenseTable = ({ users }: DenseTableProps) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { title: 'Avatar', field: 'avatar' }, + { title: 'Name', field: 'name' }, + { title: 'Email', field: 'email' }, + { title: 'Nationality', field: 'nationality' }, + ]; + + const data = users.map(user => { + return { + avatar: ( + {user.name.first} + ), + name: `${user.name.first} ${user.name.last}`, + email: user.email, + nationality: user.nat, + }; + }); + + return ( + + ); +}; + +export const ExampleFetchComponent = () => { + const { value, loading, error } = useAsync(async (): Promise => { + const response = await fetch('https://randomuser.me/api/?results=20'); + const data = await response.json(); + return data.results; + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ; +}; diff --git a/plugins/todo/src/components/ExampleFetchComponent/index.ts b/plugins/todo/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 0000000000..e7c8364039 --- /dev/null +++ b/plugins/todo/src/components/ExampleFetchComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/plugins/todo/src/index.ts b/plugins/todo/src/index.ts new file mode 100644 index 0000000000..3f43b3d453 --- /dev/null +++ b/plugins/todo/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { todoPlugin, TodoPage } from './plugin'; diff --git a/plugins/todo/src/plugin.test.ts b/plugins/todo/src/plugin.test.ts new file mode 100644 index 0000000000..ad07c17207 --- /dev/null +++ b/plugins/todo/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { todoPlugin } from './plugin'; + +describe('todo', () => { + it('should export plugin', () => { + expect(todoPlugin).toBeDefined(); + }); +}); diff --git a/plugins/todo/src/plugin.ts b/plugins/todo/src/plugin.ts new file mode 100644 index 0000000000..b991a8c929 --- /dev/null +++ b/plugins/todo/src/plugin.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Spotify AB + * + * 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, createRoutableExtension } from '@backstage/core'; + +import { rootRouteRef } from './routes'; + +export const todoPlugin = createPlugin({ + id: 'todo', + routes: { + root: rootRouteRef, + }, +}); + +export const TodoPage = todoPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/todo/src/routes.ts b/plugins/todo/src/routes.ts new file mode 100644 index 0000000000..f34eea2391 --- /dev/null +++ b/plugins/todo/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { createRouteRef } from '@backstage/core'; + +export const rootRouteRef = createRouteRef({ + title: 'todo', +}); diff --git a/plugins/todo/src/setupTests.ts b/plugins/todo/src/setupTests.ts new file mode 100644 index 0000000000..0cec5b395d --- /dev/null +++ b/plugins/todo/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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'; From 72f59d5528f435a597b2632a0cdd6bee541fa559 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 19:58:54 +0100 Subject: [PATCH 02/42] todo: add todo api types and empty client Signed-off-by: Patrik Oldsberg --- plugins/todo/package.json | 1 + plugins/todo/src/api/TodoClient.ts | 23 ++++++++++++++ plugins/todo/src/api/index.ts | 19 ++++++++++++ plugins/todo/src/api/types.ts | 49 ++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 plugins/todo/src/api/TodoClient.ts create mode 100644 plugins/todo/src/api/index.ts create mode 100644 plugins/todo/src/api/types.ts diff --git a/plugins/todo/package.json b/plugins/todo/package.json index c587fb6dc1..59b66dabb0 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -20,6 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.7.3", "@backstage/core": "^0.7.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts new file mode 100644 index 0000000000..e85e49f1fa --- /dev/null +++ b/plugins/todo/src/api/TodoClient.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { TodoApi, TodoListOptions, TodoListResult } from './types'; + +export class TodoClient implements TodoApi { + listTodos(_options: TodoListOptions): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/plugins/todo/src/api/index.ts b/plugins/todo/src/api/index.ts new file mode 100644 index 0000000000..813a161469 --- /dev/null +++ b/plugins/todo/src/api/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { TodoClient } from './TodoClient'; +export { todoApiRef } from './types'; +export type { TodoApi } from './types'; diff --git a/plugins/todo/src/api/types.ts b/plugins/todo/src/api/types.ts new file mode 100644 index 0000000000..bc6a15620b --- /dev/null +++ b/plugins/todo/src/api/types.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Spotify AB + * + * 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'; +import { Entity } from '@backstage/catalog-model'; + +export type TodoItem = { + text: string; + author?: string; + viewUrl?: string; + editUrl?: string; +}; + +export type TodoListOptions = { + entity?: Entity; + cursor?: string; +}; + +export type TodoListResult = { + items: TodoItem[]; + totalCount: number; + cursors: { + prev: string; + self: string; + next: string; + }; +}; + +export interface TodoApi { + listTodos(options: TodoListOptions): Promise; +} + +export const todoApiRef = createApiRef({ + id: 'plugin.todo.api', + description: 'Lists TODOs', +}); From 2e680b86e7929cdda4ab2f43f6a750607023f75b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 20:05:02 +0100 Subject: [PATCH 03/42] todo: hook up dev setup Signed-off-by: Patrik Oldsberg --- plugins/todo/dev/index.tsx | 29 ++++++++++++++++++++++++++--- plugins/todo/src/index.ts | 4 +++- plugins/todo/src/plugin.ts | 18 ++++++++++++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx index 3177a51d78..97742341eb 100644 --- a/plugins/todo/dev/index.tsx +++ b/plugins/todo/dev/index.tsx @@ -15,12 +15,35 @@ */ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { todoPlugin, TodoPage } from '../src/plugin'; +import { todoApiRef, todoPlugin, EntityTodoContent } from '../src'; createDevApp() .registerPlugin(todoPlugin) + .registerApi({ + api: todoApiRef, + deps: {}, + factory() { + return { + listTodos: async () => ({ + items: [ + { + text: 'Make sure this works', + author: 'Rugvip', + viewUrl: 'https://github.com/backstage/backstage', + }, + ], + totalCount: 15, + cursors: { + prev: 'prev', + self: 'self', + next: 'next', + }, + }), + }; + }, + }) .addPage({ - element: , - title: 'Root Page', + element: , + title: 'Entity Todo Content', }) .render(); diff --git a/plugins/todo/src/index.ts b/plugins/todo/src/index.ts index 3f43b3d453..be740259a8 100644 --- a/plugins/todo/src/index.ts +++ b/plugins/todo/src/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { todoPlugin, TodoPage } from './plugin'; + +export { todoApiRef } from './api'; +export { todoPlugin, EntityTodoContent } from './plugin'; diff --git a/plugins/todo/src/plugin.ts b/plugins/todo/src/plugin.ts index b991a8c929..4a202390be 100644 --- a/plugins/todo/src/plugin.ts +++ b/plugins/todo/src/plugin.ts @@ -13,18 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createPlugin, createRoutableExtension } from '@backstage/core'; +import { + createApiFactory, + createPlugin, + createRoutableExtension, +} from '@backstage/core'; +import { todoApiRef, TodoClient } from './api'; import { rootRouteRef } from './routes'; export const todoPlugin = createPlugin({ id: 'todo', + apis: [ + createApiFactory({ + api: todoApiRef, + deps: {}, + factory() { + return new TodoClient(); + }, + }), + ], routes: { root: rootRouteRef, }, }); -export const TodoPage = todoPlugin.provide( +export const EntityTodoContent = todoPlugin.provide( createRoutableExtension({ component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent), From 6ef3bc7d812feb31d1349d19514acbf058dee878 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 20:31:14 +0100 Subject: [PATCH 04/42] todo: add entity page wrapper to dev setup Signed-off-by: Patrik Oldsberg --- plugins/todo/dev/index.tsx | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx index 97742341eb..9560c49ecb 100644 --- a/plugins/todo/dev/index.tsx +++ b/plugins/todo/dev/index.tsx @@ -13,9 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { Content, Header, HeaderLabel, Page } from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; -import { todoApiRef, todoPlugin, EntityTodoContent } from '../src'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { EntityTodoContent, todoApiRef, todoPlugin } from '../src'; + +const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'backstage', + annotations: { + [LOCATION_ANNOTATION]: + 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }, + }, + spec: { + type: 'library', + }, +}; createDevApp() .registerPlugin(todoPlugin) @@ -43,7 +61,18 @@ createDevApp() }, }) .addPage({ - element: , + element: ( + + +
+ +
+ + + +
+
+ ), title: 'Entity Todo Content', }) .render(); From c6e98956a2f089da1547fbe3056047e7363c3f2b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 20:39:11 +0100 Subject: [PATCH 05/42] todo: replace example components with TodoList + test Signed-off-by: Patrik Oldsberg --- plugins/todo/package.json | 1 + .../ExampleComponent.test.tsx | 44 -------- .../ExampleComponent/ExampleComponent.tsx | 53 --------- .../ExampleFetchComponent.test.tsx | 40 ------- .../ExampleFetchComponent.tsx | 105 ------------------ .../components/ExampleFetchComponent/index.ts | 16 --- .../src/components/TodoList/TodoList.test.tsx | 46 ++++++++ .../todo/src/components/TodoList/TodoList.tsx | 76 +++++++++++++ .../{ExampleComponent => TodoList}/index.ts | 2 +- plugins/todo/src/plugin.ts | 3 +- 10 files changed, 125 insertions(+), 261 deletions(-) delete mode 100644 plugins/todo/src/components/ExampleComponent/ExampleComponent.test.tsx delete mode 100644 plugins/todo/src/components/ExampleComponent/ExampleComponent.tsx delete mode 100644 plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx delete mode 100644 plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx delete mode 100644 plugins/todo/src/components/ExampleFetchComponent/index.ts create mode 100644 plugins/todo/src/components/TodoList/TodoList.test.tsx create mode 100644 plugins/todo/src/components/TodoList/TodoList.tsx rename plugins/todo/src/components/{ExampleComponent => TodoList}/index.ts (91%) diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 59b66dabb0..de51fe2f18 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -22,6 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.3", "@backstage/core": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.1.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/todo/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/todo/src/components/ExampleComponent/ExampleComponent.test.tsx deleted file mode 100644 index 555f3be75d..0000000000 --- a/plugins/todo/src/components/ExampleComponent/ExampleComponent.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { ExampleComponent } from './ExampleComponent'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { msw, renderInTestApp } from '@backstage/test-utils'; - -describe('ExampleComponent', () => { - const server = setupServer(); - // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); - - // setup mock response - beforeEach(() => { - server.use( - rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), - ); - }); - - it('should render', async () => { - const rendered = await renderInTestApp( - - - , - ); - expect(rendered.getByText('Welcome to todo!')).toBeInTheDocument(); - }); -}); diff --git a/plugins/todo/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/todo/src/components/ExampleComponent/ExampleComponent.tsx deleted file mode 100644 index 3dc0345ee4..0000000000 --- a/plugins/todo/src/components/ExampleComponent/ExampleComponent.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { Typography, Grid } from '@material-ui/core'; -import { - InfoCard, - Header, - Page, - Content, - ContentHeader, - HeaderLabel, - SupportButton, -} from '@backstage/core'; -import { ExampleFetchComponent } from '../ExampleFetchComponent'; - -export const ExampleComponent = () => ( - -
- - -
- - - A description of your plugin goes here. - - - - - - All content should be wrapped in a card like this. - - - - - - - - -
-); diff --git a/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx deleted file mode 100644 index 6a5c0351d6..0000000000 --- a/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { render } from '@testing-library/react'; -import { ExampleFetchComponent } from './ExampleFetchComponent'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; - -describe('ExampleFetchComponent', () => { - const server = setupServer(); - // Enable sane handlers for network requests - msw.setupDefaultHandlers(server); - - // setup mock response - beforeEach(() => { - server.use( - rest.get('https://randomuser.me/*', (_, res, ctx) => - res(ctx.status(200), ctx.delay(2000), ctx.json({})), - ), - ); - }); - it('should render', async () => { - const rendered = render(); - expect(await rendered.findByTestId('progress')).toBeInTheDocument(); - }); -}); diff --git a/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx deleted file mode 100644 index 1390c8950f..0000000000 --- a/plugins/todo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { makeStyles } from '@material-ui/core/styles'; -import { Table, TableColumn, Progress } from '@backstage/core'; -import Alert from '@material-ui/lab/Alert'; -import { useAsync } from 'react-use'; - -const useStyles = makeStyles({ - avatar: { - height: 32, - width: 32, - borderRadius: '50%', - }, -}); - -type User = { - gender: string; // "male" - name: { - title: string; // "Mr", - first: string; // "Duane", - last: string; // "Reed" - }; - location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…} - email: string; // "duane.reed@example.com" - login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…} - dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37} - registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14} - phone: string; // "07-2154-5651" - cell: string; // "0405-592-879" - id: { - name: string; // "TFN", - value: string; // "796260432" - }; - picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…} - nat: string; // "AU" -}; - -type DenseTableProps = { - users: User[]; -}; - -export const DenseTable = ({ users }: DenseTableProps) => { - const classes = useStyles(); - - const columns: TableColumn[] = [ - { title: 'Avatar', field: 'avatar' }, - { title: 'Name', field: 'name' }, - { title: 'Email', field: 'email' }, - { title: 'Nationality', field: 'nationality' }, - ]; - - const data = users.map(user => { - return { - avatar: ( - {user.name.first} - ), - name: `${user.name.first} ${user.name.last}`, - email: user.email, - nationality: user.nat, - }; - }); - - return ( -
- ); -}; - -export const ExampleFetchComponent = () => { - const { value, loading, error } = useAsync(async (): Promise => { - const response = await fetch('https://randomuser.me/api/?results=20'); - const data = await response.json(); - return data.results; - }, []); - - if (loading) { - return ; - } else if (error) { - return {error.message}; - } - - return ; -}; diff --git a/plugins/todo/src/components/ExampleFetchComponent/index.ts b/plugins/todo/src/components/ExampleFetchComponent/index.ts deleted file mode 100644 index e7c8364039..0000000000 --- a/plugins/todo/src/components/ExampleFetchComponent/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/plugins/todo/src/components/TodoList/TodoList.test.tsx b/plugins/todo/src/components/TodoList/TodoList.test.tsx new file mode 100644 index 0000000000..2f44b36673 --- /dev/null +++ b/plugins/todo/src/components/TodoList/TodoList.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { renderWithEffects } from '@backstage/test-utils'; +import React from 'react'; +import { TodoApi, todoApiRef } from '../../api'; +import { TodoList } from './TodoList'; + +describe('TodoList', () => { + it('should render', async () => { + const mockApi: jest.Mocked = { + listTodos: jest.fn().mockResolvedValue({ + items: [{ text: 'My TODO' }], + totalCount: 1, + cursors: { prev: 'prev', self: 'self', next: 'next' }, + }), + }; + const mockEntity = { metadata: { name: 'mock' } } as Entity; + + const rendered = await renderWithEffects( + + + + + , + ); + + await expect(rendered.findByText('My TODO')).resolves.toBeInTheDocument(); + }); +}); diff --git a/plugins/todo/src/components/TodoList/TodoList.tsx b/plugins/todo/src/components/TodoList/TodoList.tsx new file mode 100644 index 0000000000..a27734c80b --- /dev/null +++ b/plugins/todo/src/components/TodoList/TodoList.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Progress, Table, TableColumn, useApi } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { todoApiRef } from '../../api'; +import { TodoItem } from '../../api/types'; + +const columns: TableColumn[] = [ + { title: 'Text', field: 'text' }, + { title: 'Author', field: 'author' }, + { + title: 'View', + field: 'viewUrl', + render({ viewUrl }) { + return ( + + {viewUrl} + + ); + }, + }, + { + title: 'Edit', + field: 'editUrl', + render({ editUrl }) { + return ( + + {editUrl} + + ); + }, + }, +]; + +export const TodoList = () => { + const { entity } = useEntity(); + const todoApi = useApi(todoApiRef); + + const { value, loading, error } = useAsync( + async () => todoApi.listTodos({ entity }), + [todoApi, entity], + ); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ( +
+ ); +}; diff --git a/plugins/todo/src/components/ExampleComponent/index.ts b/plugins/todo/src/components/TodoList/index.ts similarity index 91% rename from plugins/todo/src/components/ExampleComponent/index.ts rename to plugins/todo/src/components/TodoList/index.ts index 337d24d5c5..416a52fd7a 100644 --- a/plugins/todo/src/components/ExampleComponent/index.ts +++ b/plugins/todo/src/components/TodoList/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { ExampleComponent } from './ExampleComponent'; +export { TodoList } from './TodoList'; diff --git a/plugins/todo/src/plugin.ts b/plugins/todo/src/plugin.ts index 4a202390be..7cc439940c 100644 --- a/plugins/todo/src/plugin.ts +++ b/plugins/todo/src/plugin.ts @@ -40,8 +40,7 @@ export const todoPlugin = createPlugin({ export const EntityTodoContent = todoPlugin.provide( createRoutableExtension({ - component: () => - import('./components/ExampleComponent').then(m => m.ExampleComponent), + component: () => import('./components/TodoList').then(m => m.TodoList), mountPoint: rootRouteRef, }), ); From 1c04474fe7851d55fce9ab1b4fa210f8ececed2b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 20:51:41 +0100 Subject: [PATCH 06/42] todo: implement TodoClient Signed-off-by: Patrik Oldsberg --- plugins/todo/src/api/TodoClient.ts | 72 +++++++++++++++++++++++++++++- plugins/todo/src/plugin.ts | 11 +++-- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index e85e49f1fa..fb0c03bac6 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -14,10 +14,78 @@ * limitations under the License. */ +import { serializeEntityRef } from '@backstage/catalog-model'; +import { DiscoveryApi, IdentityApi } from '@backstage/core'; import { TodoApi, TodoListOptions, TodoListResult } from './types'; +interface Options { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; +} + export class TodoClient implements TodoApi { - listTodos(_options: TodoListOptions): Promise { - throw new Error('Method not implemented.'); + private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; + + constructor(options: Options) { + this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; + } + + async listTodos({ + entity, + cursor, + }: TodoListOptions): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('todo'); + const token = await this.identityApi.getIdToken(); + + const query = new URLSearchParams(); + if (entity) { + query.set('entity', serializeEntityRef(entity) as string); + } + if (cursor) { + query.set('cursor', cursor); + } + + const res = await fetch(`${baseUrl}/v1/todos?${query}`, { + headers: token + ? { + Authorization: `Bearer ${token}`, + } + : undefined, + }); + + if (!res.ok) { + const error = await this.readResponseError(res, 'list todos'); + throw error; + } + + const data: TodoListResult = await res.json(); + return data; + } + + private async readResponseError(res: Response, action: string) { + const error = new Error() as Error & { status: number }; + error.status = res.status; + + try { + const json = await res.json(); + if (typeof json?.error?.message !== 'string') { + throw new Error('invalid error'); + } + error.message = json.error.message; + if (json.error.name) { + error.name = json.error.name; + } + } catch { + try { + const text = await res.text(); + error.message = `Failed to ${action}, ${text}`; + } catch { + error.message = `Failed to ${action}, status ${res.status}`; + } + } + + throw error; } } diff --git a/plugins/todo/src/plugin.ts b/plugins/todo/src/plugin.ts index 7cc439940c..535630ba35 100644 --- a/plugins/todo/src/plugin.ts +++ b/plugins/todo/src/plugin.ts @@ -17,6 +17,8 @@ import { createApiFactory, createPlugin, createRoutableExtension, + discoveryApiRef, + identityApiRef, } from '@backstage/core'; import { todoApiRef, TodoClient } from './api'; @@ -27,9 +29,12 @@ export const todoPlugin = createPlugin({ apis: [ createApiFactory({ api: todoApiRef, - deps: {}, - factory() { - return new TodoClient(); + deps: { + identityApi: identityApiRef, + discoveryApi: discoveryApiRef, + }, + factory({ identityApi, discoveryApi }) { + return new TodoClient({ identityApi, discoveryApi }); }, }), ], From be0721c6c61fb2d22b28f336fcfa8d261af84771 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 20:55:03 +0100 Subject: [PATCH 07/42] backend: add todo plugin Signed-off-by: Patrik Oldsberg --- packages/backend/package.json | 1 + packages/backend/src/index.ts | 3 +++ packages/backend/src/plugins/todo.ts | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 packages/backend/src/plugins/todo.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index d3f967b3b3..dfbd45f200 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -41,6 +41,7 @@ "@backstage/plugin-rollbar-backend": "^0.1.7", "@backstage/plugin-scaffolder-backend": "^0.9.1", "@backstage/plugin-techdocs-backend": "^0.6.4", + "@backstage/plugin-todo-backend": "^0.1.0", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", "azure-devops-node-api": "^10.1.1", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 81d0bb96d2..23739596e8 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -43,6 +43,7 @@ import rollbar from './plugins/rollbar'; import scaffolder from './plugins/scaffolder'; import proxy from './plugins/proxy'; import techdocs from './plugins/techdocs'; +import todo from './plugins/todo'; import graphql from './plugins/graphql'; import app from './plugins/app'; import { PluginEnvironment } from './types'; @@ -77,6 +78,7 @@ async function main() { const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar')); const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); + const todoEnv = useHotMemoize(module, () => createEnv('todo')); const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes')); const kafkaEnv = useHotMemoize(module, () => createEnv('kafka')); const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); @@ -88,6 +90,7 @@ async function main() { apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); apiRouter.use('/auth', await auth(authEnv)); apiRouter.use('/techdocs', await techdocs(techdocsEnv)); + apiRouter.use('/todo', await todo(todoEnv)); apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); apiRouter.use('/kafka', await kafka(kafkaEnv)); apiRouter.use('/proxy', await proxy(proxyEnv)); diff --git a/packages/backend/src/plugins/todo.ts b/packages/backend/src/plugins/todo.ts new file mode 100644 index 0000000000..9f90d82a05 --- /dev/null +++ b/packages/backend/src/plugins/todo.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { Router } from 'express'; +import { createRouter } from '@backstage/plugin-todo-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, +}: PluginEnvironment): Promise { + return await createRouter({ logger }); +} From e24a8964f6086a6900744e803e3bacf1a35f19f5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 21:00:34 +0100 Subject: [PATCH 08/42] todo: set up both live and mocked api pages in dev env Signed-off-by: Patrik Oldsberg --- plugins/todo/dev/index.tsx | 74 +++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx index 9560c49ecb..08936f3657 100644 --- a/plugins/todo/dev/index.tsx +++ b/plugins/todo/dev/index.tsx @@ -13,10 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model'; -import { Content, Header, HeaderLabel, Page } from '@backstage/core'; +import { + ApiProvider, + ApiRegistry, + Content, + Header, + HeaderLabel, + Page, +} from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; import { EntityProvider } from '@backstage/plugin-catalog-react'; +import OnlineIcon from '@material-ui/icons/Cloud'; +import OfflineIcon from '@material-ui/icons/Storage'; import React from 'react'; import { EntityTodoContent, todoApiRef, todoPlugin } from '../src'; @@ -35,36 +45,49 @@ const entity: Entity = { }, }; +const mockedApi = { + listTodos: async () => ({ + items: [ + { + text: 'Make sure this works', + author: 'Rugvip', + viewUrl: 'https://github.com/backstage/backstage', + }, + ], + totalCount: 15, + cursors: { + prev: 'prev', + self: 'self', + next: 'next', + }, + }), +}; + createDevApp() .registerPlugin(todoPlugin) - .registerApi({ - api: todoApiRef, - deps: {}, - factory() { - return { - listTodos: async () => ({ - items: [ - { - text: 'Make sure this works', - author: 'Rugvip', - viewUrl: 'https://github.com/backstage/backstage', - }, - ], - totalCount: 15, - cursors: { - prev: 'prev', - self: 'self', - next: 'next', - }, - }), - }; - }, + .addPage({ + element: ( + + + +
+ +
+ + + +
+
+
+ ), + title: 'Entity Todo Content', + icon: OfflineIcon, }) .addPage({ element: ( -
+
@@ -73,6 +96,7 @@ createDevApp() ), - title: 'Entity Todo Content', + title: 'Backend Connected', + icon: OnlineIcon, }) .render(); From f2bf479e0f87d392c33fbcafd08f0b549163bd52 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 21:10:26 +0100 Subject: [PATCH 09/42] todo-backend: initial setup and mock API Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/package.json | 2 +- plugins/todo-backend/src/run.ts | 33 ---------- plugins/todo-backend/src/service/router.ts | 15 +++-- .../src/service/standaloneServer.ts | 62 ------------------- plugins/todo-backend/src/setupTests.ts | 17 ----- 5 files changed, 11 insertions(+), 118 deletions(-) delete mode 100644 plugins/todo-backend/src/run.ts delete mode 100644 plugins/todo-backend/src/service/standaloneServer.ts delete mode 100644 plugins/todo-backend/src/setupTests.ts diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 47c0202bba..c47ff18ab8 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -10,7 +10,6 @@ "types": "dist/index.d.ts" }, "scripts": { - "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", "lint": "backstage-cli lint", "test": "backstage-cli test", @@ -20,6 +19,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.5.5", + "@backstage/catalog-client": "^0.3.6", "@backstage/config": "^0.1.3", "@types/express": "^4.17.6", "express": "^4.17.1", diff --git a/plugins/todo-backend/src/run.ts b/plugins/todo-backend/src/run.ts deleted file mode 100644 index b96989e4b8..0000000000 --- a/plugins/todo-backend/src/run.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 { getRootLogger } from '@backstage/backend-common'; -import yn from 'yn'; -import { startStandaloneServer } from './service/standaloneServer'; - -const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; -const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); -const logger = getRootLogger(); - -startStandaloneServer({ port, enableCors, logger }).catch(err => { - logger.error(err); - process.exit(1); -}); - -process.on('SIGINT', () => { - logger.info('CTRL+C pressed; exiting.'); - process.exit(0); -}); diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index 3ea8219365..08d6440780 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -14,13 +14,18 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; export interface RouterOptions { logger: Logger; + config: Config; + reader: UrlReader; + catalogClient: CatalogClient; } export async function createRouter( @@ -31,10 +36,10 @@ export async function createRouter( const router = Router(); router.use(express.json()); - router.get('/health', (_, response) => { - logger.info('PONG!'); - response.send({ status: 'ok' }); + router.get('/v1/todos', (req, res) => { + logger.debug('got todo request', req.query); + res.json({ items: [{ text: 'Test TODO' }], totalCount: 1 }); }); - router.use(errorHandler()); + return router; } diff --git a/plugins/todo-backend/src/service/standaloneServer.ts b/plugins/todo-backend/src/service/standaloneServer.ts deleted file mode 100644 index 4901ec1895..0000000000 --- a/plugins/todo-backend/src/service/standaloneServer.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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. - */ -/* - * Copyright 2020 Spotify AB - * - * 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 { createServiceBuilder } from '@backstage/backend-common'; -import { Server } from 'http'; -import { Logger } from 'winston'; -import { createRouter } from './router'; - -export interface ServerOptions { - port: number; - enableCors: boolean; - logger: Logger; -} - -export async function startStandaloneServer( - options: ServerOptions, -): Promise { - const logger = options.logger.child({ service: 'todo-backend' }); - logger.debug('Starting application server...'); - const router = await createRouter({ - logger, - }); - - const service = createServiceBuilder(module) - .enableCors({ origin: 'http://localhost:3000' }) - .addRouter('/todo', router); - - return await service.start().catch(err => { - logger.error(err); - process.exit(1); - }); -} - -module.hot?.accept(); diff --git a/plugins/todo-backend/src/setupTests.ts b/plugins/todo-backend/src/setupTests.ts deleted file mode 100644 index ba33cf996b..0000000000 --- a/plugins/todo-backend/src/setupTests.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 {}; From 117286b313758b0d7c4fbf98d54deeedf1f13688 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 21:49:52 +0100 Subject: [PATCH 10/42] todo-backend: create layered shell with types and mock Signed-off-by: Patrik Oldsberg --- packages/backend/src/plugins/todo.ts | 22 +++++++- plugins/todo-backend/package.json | 1 + plugins/todo-backend/src/index.ts | 3 +- .../src/lib/TodoReader/TodoScmReader.ts | 40 ++++++++++++++ .../todo-backend/src/lib/TodoReader/index.ts | 23 ++++++++ .../todo-backend/src/lib/TodoReader/types.ts | 43 +++++++++++++++ plugins/todo-backend/src/lib/index.ts | 17 ++++++ .../src/service/TodoReaderService.ts | 53 +++++++++++++++++++ plugins/todo-backend/src/service/index.ts | 19 +++++++ plugins/todo-backend/src/service/router.ts | 38 ++++++++----- plugins/todo-backend/src/service/types.ts | 37 +++++++++++++ 11 files changed, 281 insertions(+), 15 deletions(-) create mode 100644 plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts create mode 100644 plugins/todo-backend/src/lib/TodoReader/index.ts create mode 100644 plugins/todo-backend/src/lib/TodoReader/types.ts create mode 100644 plugins/todo-backend/src/lib/index.ts create mode 100644 plugins/todo-backend/src/service/TodoReaderService.ts create mode 100644 plugins/todo-backend/src/service/index.ts create mode 100644 plugins/todo-backend/src/service/types.ts diff --git a/packages/backend/src/plugins/todo.ts b/packages/backend/src/plugins/todo.ts index 9f90d82a05..38fb43f78e 100644 --- a/packages/backend/src/plugins/todo.ts +++ b/packages/backend/src/plugins/todo.ts @@ -13,12 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { CatalogClient } from '@backstage/catalog-client'; +import { + createRouter, + TodoReaderService, + TodoScmReader, +} from '@backstage/plugin-todo-backend'; import { Router } from 'express'; -import { createRouter } from '@backstage/plugin-todo-backend'; import { PluginEnvironment } from '../types'; export default async function createPlugin({ logger, + reader, + discovery, }: PluginEnvironment): Promise { - return await createRouter({ logger }); + const todoReader = new TodoScmReader({ + logger, + reader, + }); + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const todoService = new TodoReaderService({ + logger, + todoReader, + catalogClient, + }); + + return await createRouter({ todoService }); } diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index c47ff18ab8..6f3d88f56d 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -20,6 +20,7 @@ "dependencies": { "@backstage/backend-common": "^0.5.5", "@backstage/catalog-client": "^0.3.6", + "@backstage/catalog-model": "^0.7.3", "@backstage/config": "^0.1.3", "@types/express": "^4.17.6", "express": "^4.17.1", diff --git a/plugins/todo-backend/src/index.ts b/plugins/todo-backend/src/index.ts index 7612c392a2..d0406483c8 100644 --- a/plugins/todo-backend/src/index.ts +++ b/plugins/todo-backend/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export * from './service/router'; +export * from './lib'; +export * from './service'; diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts new file mode 100644 index 0000000000..bf309cd673 --- /dev/null +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { UrlReader } from '@backstage/backend-common'; +import { Logger } from 'winston'; +import { ReadTodosOptions, ReadTodosResult, TodoReader } from './types'; + +type Options = { + logger: Logger; + reader: UrlReader; +}; + +export class TodoScmReader implements TodoReader { + private readonly logger: Logger; + private readonly reader: UrlReader; + + constructor(options: Options) { + this.logger = options.logger; + this.reader = options.reader; + } + + async readTodos(_options: ReadTodosOptions): Promise { + return { + items: [{ text: 'My mock todo' }], + }; + } +} diff --git a/plugins/todo-backend/src/lib/TodoReader/index.ts b/plugins/todo-backend/src/lib/TodoReader/index.ts new file mode 100644 index 0000000000..22c47ebef2 --- /dev/null +++ b/plugins/todo-backend/src/lib/TodoReader/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { TodoScmReader } from './TodoScmReader'; +export type { + TodoItem, + TodoReader, + ReadTodosOptions, + ReadTodosResult, +} from './types'; diff --git a/plugins/todo-backend/src/lib/TodoReader/types.ts b/plugins/todo-backend/src/lib/TodoReader/types.ts new file mode 100644 index 0000000000..46e7f1dcfa --- /dev/null +++ b/plugins/todo-backend/src/lib/TodoReader/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 TodoItem = { + text: string; + author?: string; + viewUrl?: string; + editUrl?: string; +}; + +export type ReadTodosOptions = { + /** + * Base URLs defining the root at which to search for TODOs + */ + url: string; +}; + +export type ReadTodosResult = { + /** + * TODO items found at the given locations + */ + items: TodoItem[]; +}; + +export interface TodoReader { + /** + * Searches for TODO items in code at a given location + */ + readTodos(options: ReadTodosOptions): Promise; +} diff --git a/plugins/todo-backend/src/lib/index.ts b/plugins/todo-backend/src/lib/index.ts new file mode 100644 index 0000000000..85c100c959 --- /dev/null +++ b/plugins/todo-backend/src/lib/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './TodoReader'; diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts new file mode 100644 index 0000000000..4d9afc3099 --- /dev/null +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { CatalogClient } from '@backstage/catalog-client'; +import { Logger } from 'winston'; +import { TodoReader } from '../lib'; +import { ListTodosRequest, ListTodosResponse, TodoService } from './types'; + +type Options = { + logger: Logger; + todoReader: TodoReader; + catalogClient: CatalogClient; +}; + +export class TodoReaderService implements TodoService { + private readonly logger: Logger; + private readonly todoReader: TodoReader; + private readonly catalogClient: CatalogClient; + + constructor(options: Options) { + this.logger = options.logger; + this.todoReader = options.todoReader; + this.catalogClient = options.catalogClient; + } + + async listTodos(_req: ListTodosRequest): Promise { + const todos = await this.todoReader.readTodos({ + url: 'https://github.com/backstage/backstage', + }); + return { + items: todos.items.slice(0, 10), + totalCount: todos.items.length, + cursors: { + prev: 'prev', + self: 'self', + next: 'next', + }, + }; + } +} diff --git a/plugins/todo-backend/src/service/index.ts b/plugins/todo-backend/src/service/index.ts new file mode 100644 index 0000000000..cbcf591da5 --- /dev/null +++ b/plugins/todo-backend/src/service/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { createRouter } from './router'; +export type { TodoService, ListTodosRequest, ListTodosResponse } from './types'; +export { TodoReaderService } from './TodoReaderService'; diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index 08d6440780..f5237d6c0a 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -14,31 +14,45 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; -import { CatalogClient } from '@backstage/catalog-client'; -import { Config } from '@backstage/config'; +import { InputError } from '@backstage/backend-common'; +import { EntityName, parseEntityName } from '@backstage/catalog-model'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; +import { TodoService } from './types'; export interface RouterOptions { - logger: Logger; - config: Config; - reader: UrlReader; - catalogClient: CatalogClient; + todoService: TodoService; } export async function createRouter( options: RouterOptions, ): Promise { - const { logger } = options; + const { todoService } = options; const router = Router(); router.use(express.json()); - router.get('/v1/todos', (req, res) => { - logger.debug('got todo request', req.query); - res.json({ items: [{ text: 'Test TODO' }], totalCount: 1 }); + router.get('/v1/todos', async (req, res) => { + const { entity: entityRef, cursor } = req.query; + + if (entityRef && typeof entityRef !== 'string') { + throw new InputError(`entity query must be a string`); + } + if (cursor && typeof cursor !== 'string') { + throw new InputError(`cursor query must be a string`); + } + + let entity: EntityName | undefined = undefined; + if (entityRef) { + try { + entity = parseEntityName(entityRef); + } catch (error) { + throw new InputError(`Invalid entity ref, ${error}`); + } + } + + const todos = await todoService.listTodos({ entity, cursor }); + res.json(todos); }); return router; diff --git a/plugins/todo-backend/src/service/types.ts b/plugins/todo-backend/src/service/types.ts new file mode 100644 index 0000000000..bf417f0895 --- /dev/null +++ b/plugins/todo-backend/src/service/types.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { EntityName } from '@backstage/catalog-model'; +import { TodoItem } from '../lib'; + +export type ListTodosRequest = { + entity?: EntityName; + cursor?: string; +}; + +export type ListTodosResponse = { + items: TodoItem[]; + totalCount: number; + cursors: { + prev: string; + self: string; + next: string; + }; +}; + +export interface TodoService { + listTodos(req: ListTodosRequest): Promise; +} From c829e1e983f8089ca4836c3b0c948471eda2b4e0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 22:15:29 +0100 Subject: [PATCH 11/42] todo: fix error response parsing and set NotFoundError name Signed-off-by: Patrik Oldsberg --- plugins/todo/src/api/TodoClient.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index fb0c03bac6..b8caee3ee9 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -69,7 +69,7 @@ export class TodoClient implements TodoApi { error.status = res.status; try { - const json = await res.json(); + const json = await res.clone().json(); if (typeof json?.error?.message !== 'string') { throw new Error('invalid error'); } @@ -84,6 +84,9 @@ export class TodoClient implements TodoApi { } catch { error.message = `Failed to ${action}, status ${res.status}`; } + if (res.status === 404) { + error.name = 'NotFoundError'; + } } throw error; From 76e6336312aee1ec0132194fc886a5449545e7b8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 22:18:04 +0100 Subject: [PATCH 12/42] todo-backend: full TodoReaderService implementation Signed-off-by: Patrik Oldsberg --- packages/backend/src/plugins/todo.ts | 1 - .../src/lib/TodoReader/TodoScmReader.ts | 4 +- .../src/service/TodoReaderService.ts | 123 ++++++++++++++++-- 3 files changed, 111 insertions(+), 17 deletions(-) diff --git a/packages/backend/src/plugins/todo.ts b/packages/backend/src/plugins/todo.ts index 38fb43f78e..6141108a27 100644 --- a/packages/backend/src/plugins/todo.ts +++ b/packages/backend/src/plugins/todo.ts @@ -33,7 +33,6 @@ export default async function createPlugin({ }); const catalogClient = new CatalogClient({ discoveryApi: discovery }); const todoService = new TodoReaderService({ - logger, todoReader, catalogClient, }); diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index bf309cd673..f5457d0dd3 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -32,9 +32,9 @@ export class TodoScmReader implements TodoReader { this.reader = options.reader; } - async readTodos(_options: ReadTodosOptions): Promise { + async readTodos({ url }: ReadTodosOptions): Promise { return { - items: [{ text: 'My mock todo' }], + items: [{ text: 'My mock todo', viewUrl: url }], }; } } diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 4d9afc3099..e646a29634 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -14,40 +14,135 @@ * limitations under the License. */ +import { InputError, NotFoundError } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; -import { Logger } from 'winston'; +import { + LOCATION_ANNOTATION, + SOURCE_LOCATION_ANNOTATION, + serializeEntityRef, + Entity, + parseLocationReference, +} from '@backstage/catalog-model'; import { TodoReader } from '../lib'; import { ListTodosRequest, ListTodosResponse, TodoService } from './types'; +const DEFAULT_DEFAULT_PAGE_SIZE = 10; +const DEFAULT_MAX_PAGE_SIZE = 50; + type Options = { - logger: Logger; todoReader: TodoReader; catalogClient: CatalogClient; + maxPageSize?: number; + defaultPageSize?: number; }; export class TodoReaderService implements TodoService { - private readonly logger: Logger; private readonly todoReader: TodoReader; private readonly catalogClient: CatalogClient; + private readonly maxPageSize: number; + private readonly defaultPageSize: number; constructor(options: Options) { - this.logger = options.logger; this.todoReader = options.todoReader; this.catalogClient = options.catalogClient; + this.maxPageSize = options.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE; + this.defaultPageSize = options.defaultPageSize ?? DEFAULT_DEFAULT_PAGE_SIZE; } - async listTodos(_req: ListTodosRequest): Promise { - const todos = await this.todoReader.readTodos({ - url: 'https://github.com/backstage/backstage', - }); + async listTodos({ + entity: entityName, + cursor, + }: ListTodosRequest): Promise { + if (!entityName) { + throw new InputError('entity filter is required to list todos'); + } + const entity = await this.catalogClient.getEntityByName(entityName); + if (!entity) { + throw new NotFoundError( + `Entity not found, ${serializeEntityRef(entityName)}`, + ); + } + + const url = this.getEntitySourceUrl(entity); + const todos = await this.todoReader.readTodos({ url }); + + const { offset, limit } = this.parseCursor(cursor); return { - items: todos.items.slice(0, 10), + items: todos.items.slice(offset, offset + limit), totalCount: todos.items.length, - cursors: { - prev: 'prev', - self: 'self', - next: 'next', - }, + cursors: this.calculateCursors(offset, limit, todos.items.length), + }; + } + + private getEntitySourceUrl(entity: Entity) { + const sourceLocation = + entity.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION]; + if (sourceLocation) { + const parsed = parseLocationReference(sourceLocation); + if (parsed.type !== 'url') { + throw new InputError( + `Invalid entity source location type for ${serializeEntityRef( + entity, + )}, got ${parsed.type}`, + ); + } + return parsed.target; + } + + const location = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + if (location) { + const parsed = parseLocationReference(location); + if (parsed.type !== 'url') { + throw new InputError( + `Invalid entity source location type for ${serializeEntityRef( + entity, + )}, got ${parsed.type}`, + ); + } + return parsed.target; + } + throw new InputError( + `No entity location annotation found for ${serializeEntityRef(entity)}`, + ); + } + + private parseCursor( + cursor: string | undefined, + ): { offset: number; limit: number } { + if (!cursor) { + return { offset: 0, limit: this.defaultPageSize }; + } + + const [offsetStr, limitStr] = cursor.split(','); + + const offset = parseInt(offsetStr, 10); + if (!Number.isInteger(offset) || offset < 0) { + throw new InputError(`Invalid cursor, ${cursor}`); + } + + let limit = parseInt(limitStr, 10); + if (!Number.isInteger(limit) || limit < 0) { + throw new InputError(`Invalid cursor, ${cursor}`); + } + if (limit > this.maxPageSize) { + limit = this.maxPageSize; + } + + return { offset, limit }; + } + + private calculateCursors( + offset: number, + limit: number, + total: number, + ): ListTodosResponse['cursors'] { + const prevOffset = Math.max(offset - limit, 0); + const nextOffset = Math.min(offset + limit, total - limit); + + return { + prev: `${prevOffset},${limit}`, + self: `${offset},${limit}`, + next: `${nextOffset},${limit}`, }; } } From 2eeb88435316c898dedec3fbfa948ed626240680 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Mar 2021 23:02:07 +0100 Subject: [PATCH 13/42] todo-backend: implement TodoScmReader Signed-off-by: Patrik Oldsberg --- packages/backend/src/plugins/todo.ts | 3 +- plugins/todo-backend/package.json | 8 +- .../src/lib/TodoReader/TodoScmReader.ts | 88 +++++++++++++++++-- yarn.lock | 34 ++++++- 4 files changed, 123 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/plugins/todo.ts b/packages/backend/src/plugins/todo.ts index 6141108a27..84f2feac92 100644 --- a/packages/backend/src/plugins/todo.ts +++ b/packages/backend/src/plugins/todo.ts @@ -25,9 +25,10 @@ import { PluginEnvironment } from '../types'; export default async function createPlugin({ logger, reader, + config, discovery, }: PluginEnvironment): Promise { - const todoReader = new TodoScmReader({ + const todoReader = TodoScmReader.fromConfig(config, { logger, reader, }); diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 6f3d88f56d..9fd5f68948 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -22,18 +22,20 @@ "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.3", "@backstage/config": "^0.1.3", + "@backstage/integration": "^0.5.0", "@types/express": "^4.17.6", + "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", + "leasot": "^11.5.0", "winston": "^3.2.1", - "cross-fetch": "^3.0.6", "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "^0.6.3", "@types/supertest": "^2.0.8", - "supertest": "^4.0.2", - "msw": "^0.21.2" + "msw": "^0.21.2", + "supertest": "^4.0.2" }, "files": [ "dist" diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index f5457d0dd3..5450d0228b 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -14,27 +14,105 @@ * limitations under the License. */ +import { extname } from 'path'; import { UrlReader } from '@backstage/backend-common'; +import { ScmIntegrations } from '@backstage/integration'; import { Logger } from 'winston'; -import { ReadTodosOptions, ReadTodosResult, TodoReader } from './types'; +import { parse } from 'leasot'; +import { + ReadTodosOptions, + ReadTodosResult, + TodoItem, + TodoReader, +} from './types'; +import { Config } from '@backstage/config'; + +type TodoParser = (ctx: { + content: string; + path: string; +}) => (TodoItem & { line: number })[]; type Options = { logger: Logger; reader: UrlReader; + parser?: TodoParser; +}; + +const defaultTodoParser: TodoParser = ({ content, path }) => { + try { + const comments = parse(content, { + extension: extname(path), + }); + + return comments.map(comment => ({ + text: comment.text, + author: comment.ref, + line: comment.line, + })); + } catch /* ignore unsupported extensions */ { + return []; + } }; export class TodoScmReader implements TodoReader { private readonly logger: Logger; private readonly reader: UrlReader; + private readonly parser: TodoParser; + private readonly integrations: ScmIntegrations; - constructor(options: Options) { + static fromConfig(config: Config, options: Options) { + return new TodoScmReader(options, ScmIntegrations.fromConfig(config)); + } + + private constructor(options: Options, integrations: ScmIntegrations) { this.logger = options.logger; this.reader = options.reader; + this.parser = options.parser ?? defaultTodoParser; + this.integrations = integrations; } async readTodos({ url }: ReadTodosOptions): Promise { - return { - items: [{ text: 'My mock todo', viewUrl: url }], - }; + const tree = await this.reader.readTree(url, { + filter(path) { + return !path.startsWith('.yarn'); + }, + }); + + const files = await tree.files(); + this.logger.info(`Read ${files.length} files from ${url}`); + + const todos = new Array(); + for (const file of files) { + const content = await file.content(); + try { + const items = this.parser({ + path: file.path, + content: content.toString('utf8'), + }); + const viewUrl = this.integrations.resolveUrl({ + url: file.path, + base: url, + }); + + let editUrl: string | undefined = this.integrations.resolveEditUrl( + viewUrl, + ); + if (editUrl === viewUrl) { + editUrl = undefined; + } + + todos.push( + ...items.map(item => ({ + ...item, + editUrl, + viewUrl: item.line ? `${viewUrl}#L${item.line}` : viewUrl, + })), + ); + } catch (error) { + this.logger.error(`Failed to parse TODO in ${url}, ${error}`); + } + } + + return { items: todos }; } } diff --git a/yarn.lock b/yarn.lock index 0b5a3e2fa0..c056641ff7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10092,7 +10092,7 @@ commander@^5.0.0, commander@^5.1.0: resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^6.1.0: +commander@^6.1.0, commander@^6.2.1: version "6.2.1" resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== @@ -12212,6 +12212,11 @@ envinfo@^7.7.4: resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.7.4.tgz#c6311cdd38a0e86808c1c9343f667e4267c4a320" integrity sha512-TQXTYFVVwwluWSFis6K2XKxgrD22jEv0FTuLCQI+OjH7rn93+iY0fSSFM5lrSxFY+H1+B0/cvvlamr3UsBivdQ== +eol@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz#f701912f504074be35c6117a5c4ade49cd547acd" + integrity sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg== + err-code@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" @@ -13890,6 +13895,11 @@ get-stdin@^6.0.0: resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + get-stream@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -16881,6 +16891,11 @@ json-to-pretty-yaml@^1.2.2: remedial "^1.0.7" remove-trailing-spaces "^1.0.6" +json2xml@^0.1.3: + version "0.1.3" + resolved "https://registry.npmjs.org/json2xml/-/json2xml-0.1.3.tgz#9ae7c220bedd7c66a668e26f7ac182f6704eca21" + integrity sha1-mufCIL7dfGamaOJvesGC9nBOyiE= + json3@^3.3.2: version "3.3.3" resolved "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" @@ -17295,6 +17310,23 @@ ldapjs@^2.2.0: vasync "^2.2.0" verror "^1.8.1" +leasot@^11.5.0: + version "11.5.0" + resolved "https://registry.npmjs.org/leasot/-/leasot-11.5.0.tgz#a99eb4479618c9d2ea442a32ee006e5b9da4844d" + integrity sha512-L08QKlmofYIRs5gfOmhOtbEJUu6U/zFGvYpboPq34yAHQ2Oc/QznOw62noe29yRJLiV/XnIDS8vO2um1e1sikA== + dependencies: + async "^3.2.0" + chalk "^4.1.0" + commander "^6.2.1" + eol "^0.9.1" + get-stdin "^8.0.0" + globby "^11.0.1" + json2xml "^0.1.3" + lodash "^4.17.20" + log-symbols "^4.0.0" + strip-ansi "^6.0.0" + text-table "^0.2.0" + left-pad@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" From cf193329a446ff3f8c0acb8be4f41135a3da3282 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 00:37:48 +0100 Subject: [PATCH 14/42] todo,todo-backend: switch to and implement offset/limit pagination Signed-off-by: Patrik Oldsberg --- .../src/service/TodoReaderService.ts | 72 ++++++------------- plugins/todo-backend/src/service/router.ts | 24 +++++-- plugins/todo-backend/src/service/types.ts | 10 ++- plugins/todo/dev/index.tsx | 7 +- plugins/todo/src/api/TodoClient.ts | 10 ++- plugins/todo/src/api/types.ts | 10 ++- .../todo/src/components/TodoList/TodoList.tsx | 23 ++++-- 7 files changed, 75 insertions(+), 81 deletions(-) diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index e646a29634..57010ee7e0 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -49,28 +49,40 @@ export class TodoReaderService implements TodoService { this.defaultPageSize = options.defaultPageSize ?? DEFAULT_DEFAULT_PAGE_SIZE; } - async listTodos({ - entity: entityName, - cursor, - }: ListTodosRequest): Promise { - if (!entityName) { + async listTodos(req: ListTodosRequest): Promise { + if (!req.entity) { throw new InputError('entity filter is required to list todos'); } - const entity = await this.catalogClient.getEntityByName(entityName); + const entity = await this.catalogClient.getEntityByName(req.entity); if (!entity) { throw new NotFoundError( - `Entity not found, ${serializeEntityRef(entityName)}`, + `Entity not found, ${serializeEntityRef(req.entity)}`, ); } const url = this.getEntitySourceUrl(entity); const todos = await this.todoReader.readTodos({ url }); + const totalCount = todos.items.length; + + let limit = req.limit ?? this.defaultPageSize; + if (limit < 0) { + limit = 0; + } else if (limit > this.maxPageSize) { + limit = this.maxPageSize; + } + + let offset = req.offset ?? 0; + if (offset < 0) { + offset = 0; + } else if (offset - limit > totalCount) { + offset = totalCount - limit; + } - const { offset, limit } = this.parseCursor(cursor); return { items: todos.items.slice(offset, offset + limit), - totalCount: todos.items.length, - cursors: this.calculateCursors(offset, limit, todos.items.length), + totalCount, + offset, + limit, }; } @@ -105,44 +117,4 @@ export class TodoReaderService implements TodoService { `No entity location annotation found for ${serializeEntityRef(entity)}`, ); } - - private parseCursor( - cursor: string | undefined, - ): { offset: number; limit: number } { - if (!cursor) { - return { offset: 0, limit: this.defaultPageSize }; - } - - const [offsetStr, limitStr] = cursor.split(','); - - const offset = parseInt(offsetStr, 10); - if (!Number.isInteger(offset) || offset < 0) { - throw new InputError(`Invalid cursor, ${cursor}`); - } - - let limit = parseInt(limitStr, 10); - if (!Number.isInteger(limit) || limit < 0) { - throw new InputError(`Invalid cursor, ${cursor}`); - } - if (limit > this.maxPageSize) { - limit = this.maxPageSize; - } - - return { offset, limit }; - } - - private calculateCursors( - offset: number, - limit: number, - total: number, - ): ListTodosResponse['cursors'] { - const prevOffset = Math.max(offset - limit, 0); - const nextOffset = Math.min(offset + limit, total - limit); - - return { - prev: `${prevOffset},${limit}`, - self: `${offset},${limit}`, - next: `${nextOffset},${limit}`, - }; - } } diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index f5237d6c0a..12655cb208 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -33,15 +33,13 @@ export async function createRouter( router.use(express.json()); router.get('/v1/todos', async (req, res) => { - const { entity: entityRef, cursor } = req.query; + const offset = parseIntegerParam(req.query.offset, 'offset query'); + const limit = parseIntegerParam(req.query.limit, 'limit query'); + const entityRef = req.query.entity; if (entityRef && typeof entityRef !== 'string') { throw new InputError(`entity query must be a string`); } - if (cursor && typeof cursor !== 'string') { - throw new InputError(`cursor query must be a string`); - } - let entity: EntityName | undefined = undefined; if (entityRef) { try { @@ -51,9 +49,23 @@ export async function createRouter( } } - const todos = await todoService.listTodos({ entity, cursor }); + const todos = await todoService.listTodos({ entity, offset, limit }); res.json(todos); }); return router; } + +function parseIntegerParam(str: unknown, ctx: string): number | undefined { + if (str === undefined) { + return undefined; + } + if (typeof str !== 'string') { + throw new InputError(`invalid ${ctx}, must be a string`); + } + const parsed = parseInt(str, 10); + if (!Number.isInteger(parsed)) { + throw new InputError(`invalid ${ctx}, not an integer`); + } + return parsed; +} diff --git a/plugins/todo-backend/src/service/types.ts b/plugins/todo-backend/src/service/types.ts index bf417f0895..ac8ecc16b5 100644 --- a/plugins/todo-backend/src/service/types.ts +++ b/plugins/todo-backend/src/service/types.ts @@ -19,17 +19,15 @@ import { TodoItem } from '../lib'; export type ListTodosRequest = { entity?: EntityName; - cursor?: string; + offset?: number; + limit?: number; }; export type ListTodosResponse = { items: TodoItem[]; totalCount: number; - cursors: { - prev: string; - self: string; - next: string; - }; + offset: number; + limit: number; }; export interface TodoService { diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx index 08936f3657..25a6769cb5 100644 --- a/plugins/todo/dev/index.tsx +++ b/plugins/todo/dev/index.tsx @@ -55,11 +55,8 @@ const mockedApi = { }, ], totalCount: 15, - cursors: { - prev: 'prev', - self: 'self', - next: 'next', - }, + offset: 0, + limit: 10, }), }; diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index b8caee3ee9..44d80ba22e 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -34,7 +34,8 @@ export class TodoClient implements TodoApi { async listTodos({ entity, - cursor, + offset, + limit, }: TodoListOptions): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('todo'); const token = await this.identityApi.getIdToken(); @@ -43,8 +44,11 @@ export class TodoClient implements TodoApi { if (entity) { query.set('entity', serializeEntityRef(entity) as string); } - if (cursor) { - query.set('cursor', cursor); + if (typeof offset === 'number') { + query.set('offset', String(offset)); + } + if (typeof limit === 'number') { + query.set('limit', String(limit)); } const res = await fetch(`${baseUrl}/v1/todos?${query}`, { diff --git a/plugins/todo/src/api/types.ts b/plugins/todo/src/api/types.ts index bc6a15620b..890bc50731 100644 --- a/plugins/todo/src/api/types.ts +++ b/plugins/todo/src/api/types.ts @@ -26,17 +26,15 @@ export type TodoItem = { export type TodoListOptions = { entity?: Entity; - cursor?: string; + offset?: number; + limit?: number; }; export type TodoListResult = { items: TodoItem[]; totalCount: number; - cursors: { - prev: string; - self: string; - next: string; - }; + offset: number; + limit: number; }; export interface TodoApi { diff --git a/plugins/todo/src/components/TodoList/TodoList.tsx b/plugins/todo/src/components/TodoList/TodoList.tsx index a27734c80b..df0413107a 100644 --- a/plugins/todo/src/components/TodoList/TodoList.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.tsx @@ -17,7 +17,7 @@ import { Progress, Table, TableColumn, useApi } from '@backstage/core'; import { useEntity } from '@backstage/plugin-catalog-react'; import Alert from '@material-ui/lab/Alert'; -import React from 'react'; +import React, { useState } from 'react'; import { useAsync } from 'react-use'; import { todoApiRef } from '../../api'; import { TodoItem } from '../../api/types'; @@ -52,10 +52,17 @@ const columns: TableColumn[] = [ export const TodoList = () => { const { entity } = useEntity(); const todoApi = useApi(todoApiRef); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(10); const { value, loading, error } = useAsync( - async () => todoApi.listTodos({ entity }), - [todoApi, entity], + async () => + todoApi.listTodos({ + entity, + offset: page * pageSize, + limit: pageSize, + }), + [todoApi, entity, page, pageSize], ); if (loading) { @@ -67,9 +74,15 @@ export const TodoList = () => { return (
); From 8e47b7d988f02219a29d5779d794ffe4935486dd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 00:46:21 +0100 Subject: [PATCH 15/42] todo-backend: cache read results Signed-off-by: Patrik Oldsberg --- .../src/lib/TodoReader/TodoScmReader.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 5450d0228b..8caa853c6b 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -38,6 +38,11 @@ type Options = { parser?: TodoParser; }; +type CacheItem = { + etag: string; + result: ReadTodosResult; +}; + const defaultTodoParser: TodoParser = ({ content, path }) => { try { const comments = parse(content, { @@ -60,6 +65,8 @@ export class TodoScmReader implements TodoReader { private readonly parser: TodoParser; private readonly integrations: ScmIntegrations; + private readonly cache = new Map(); + static fromConfig(config: Config, options: Options) { return new TodoScmReader(options, ScmIntegrations.fromConfig(config)); } @@ -72,7 +79,25 @@ export class TodoScmReader implements TodoReader { } async readTodos({ url }: ReadTodosOptions): Promise { + const cacheItem = this.cache.get(url); + try { + const newCacheItem = await this.doReadTodos({ url }, cacheItem?.etag); + this.cache.set(url, newCacheItem); + return newCacheItem.result; + } catch (error) { + if (cacheItem && error.name === 'NotModifiedError') { + return cacheItem.result; + } + throw error; + } + } + + private async doReadTodos( + { url }: ReadTodosOptions, + etag?: string, + ): Promise { const tree = await this.reader.readTree(url, { + etag, filter(path) { return !path.startsWith('.yarn'); }, @@ -113,6 +138,6 @@ export class TodoScmReader implements TodoReader { } } - return { items: todos }; + return { result: { items: todos }, etag: tree.etag }; } } From 74543b6688e82116f18a49de08877aa59be5d581 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 00:53:55 +0100 Subject: [PATCH 16/42] todo-backend: add service tests Signed-off-by: Patrik Oldsberg --- .../todo-backend/src/service/router.test.ts | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/plugins/todo-backend/src/service/router.test.ts b/plugins/todo-backend/src/service/router.test.ts index 0aaeafa379..1bd70c1643 100644 --- a/plugins/todo-backend/src/service/router.test.ts +++ b/plugins/todo-backend/src/service/router.test.ts @@ -14,18 +14,28 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; +import { TodoService } from './types'; + +const mockListBody = { + items: [{ text: 'my todo' }], + totalCount: 1, + offset: 0, + limit: 10, +}; describe('createRouter', () => { let app: express.Express; + const mockService: jest.Mocked = { + listTodos: jest.fn(), + }; beforeAll(async () => { const router = await createRouter({ - logger: getVoidLogger(), + todoService: mockService, }); app = express().use(router); }); @@ -35,11 +45,49 @@ describe('createRouter', () => { }); describe('GET /health', () => { - it('returns ok', async () => { - const response = await request(app).get('/health'); + it('returns list without query', async () => { + mockService.listTodos.mockResolvedValueOnce(mockListBody); + const response = await request(app).get('/v1/todos'); expect(response.status).toEqual(200); - expect(response.body).toEqual({ status: 'ok' }); + expect(response.body).toEqual(mockListBody); + expect(mockService.listTodos).toHaveBeenCalledWith({ + entity: undefined, + offset: undefined, + limit: undefined, + }); + }); + + it('forwards pagination query', async () => { + mockService.listTodos.mockResolvedValueOnce(mockListBody); + + const response = await request(app).get('/v1/todos?offset=5&limit=3'); + expect(response.status).toEqual(200); + expect(response.body).toEqual(mockListBody); + expect(mockService.listTodos).toHaveBeenCalledWith({ + entity: undefined, + offset: 5, + limit: 3, + }); + }); + + it('forwards entity query', async () => { + mockService.listTodos.mockResolvedValueOnce(mockListBody); + + const response = await request(app).get( + '/v1/todos?entity=component:my-component', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(mockListBody); + expect(mockService.listTodos).toHaveBeenCalledWith({ + entity: { + name: 'my-component', + kind: 'component', + namespace: 'default', + }, + offset: undefined, + limit: undefined, + }); }); }); }); From 240ab2d5204f2910b26eb3644eba1e1e804cf052 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 01:26:41 +0100 Subject: [PATCH 17/42] todo,todo-backend: tweak todo item model and table Signed-off-by: Patrik Oldsberg --- .../src/lib/TodoReader/TodoScmReader.ts | 22 ++++---- .../todo-backend/src/lib/TodoReader/types.ts | 12 ++++- plugins/todo/src/api/types.ts | 12 ++++- .../todo/src/components/TodoList/TodoList.tsx | 50 +++++++++++-------- 4 files changed, 60 insertions(+), 36 deletions(-) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 8caa853c6b..919cf3dd18 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -30,7 +30,7 @@ import { Config } from '@backstage/config'; type TodoParser = (ctx: { content: string; path: string; -}) => (TodoItem & { line: number })[]; +}) => { text: string; author?: string; lineNumber: number }[]; type Options = { logger: Logger; @@ -52,7 +52,7 @@ const defaultTodoParser: TodoParser = ({ content, path }) => { return comments.map(comment => ({ text: comment.text, author: comment.ref, - line: comment.line, + lineNumber: comment.line, })); } catch /* ignore unsupported extensions */ { return []; @@ -119,18 +119,14 @@ export class TodoScmReader implements TodoReader { base: url, }); - let editUrl: string | undefined = this.integrations.resolveEditUrl( - viewUrl, - ); - if (editUrl === viewUrl) { - editUrl = undefined; - } - todos.push( - ...items.map(item => ({ - ...item, - editUrl, - viewUrl: item.line ? `${viewUrl}#L${item.line}` : viewUrl, + ...items.map(({ lineNumber, text, author }) => ({ + text, + author, + lineNumber, + repoFilePath: file.path, + viewUrl: + lineNumber === undefined ? viewUrl : `${viewUrl}#L${lineNumber}`, })), ); } catch (error) { diff --git a/plugins/todo-backend/src/lib/TodoReader/types.ts b/plugins/todo-backend/src/lib/TodoReader/types.ts index 46e7f1dcfa..499d3ac843 100644 --- a/plugins/todo-backend/src/lib/TodoReader/types.ts +++ b/plugins/todo-backend/src/lib/TodoReader/types.ts @@ -15,10 +15,20 @@ */ export type TodoItem = { + /** The contents of the TODO comment */ text: string; + + /** References author, if any */ author?: string; + + /** URL used to view the file */ viewUrl?: string; - editUrl?: string; + + /** The line number of the file that the TODO occurs at */ + lineNumber?: number; + + /** The path of the file containing the TODO within the repo */ + repoFilePath?: string; }; export type ReadTodosOptions = { diff --git a/plugins/todo/src/api/types.ts b/plugins/todo/src/api/types.ts index 890bc50731..759df24b60 100644 --- a/plugins/todo/src/api/types.ts +++ b/plugins/todo/src/api/types.ts @@ -18,10 +18,20 @@ import { createApiRef } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; export type TodoItem = { + /** The contents of the TODO comment */ text: string; + + /** References author, if any */ author?: string; + + /** URL used to view the file */ viewUrl?: string; - editUrl?: string; + + /** The line number of the file that the TODO occurs at */ + lineNumber?: number; + + /** The path of the file containing the TODO within the repo */ + repoFilePath?: string; }; export type TodoListOptions = { diff --git a/plugins/todo/src/components/TodoList/TodoList.tsx b/plugins/todo/src/components/TodoList/TodoList.tsx index df0413107a..1201dca4fd 100644 --- a/plugins/todo/src/components/TodoList/TodoList.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.tsx @@ -14,7 +14,14 @@ * limitations under the License. */ -import { Progress, Table, TableColumn, useApi } from '@backstage/core'; +import { + Progress, + Table, + TableColumn, + useApi, + OverflowTooltip, + Link, +} from '@backstage/core'; import { useEntity } from '@backstage/plugin-catalog-react'; import Alert from '@material-ui/lab/Alert'; import React, { useState } from 'react'; @@ -23,29 +30,29 @@ import { todoApiRef } from '../../api'; import { TodoItem } from '../../api/types'; const columns: TableColumn[] = [ - { title: 'Text', field: 'text' }, - { title: 'Author', field: 'author' }, { - title: 'View', - field: 'viewUrl', - render({ viewUrl }) { - return ( - - {viewUrl} - - ); - }, + title: 'Text', + width: '100%', + highlight: true, + render: ({ text }) => , }, { - title: 'Edit', - field: 'editUrl', - render({ editUrl }) { - return ( - - {editUrl} - - ); - }, + title: 'File', + width: '80%', + render: ({ viewUrl, repoFilePath }) => + viewUrl ? ( + + + + ) : ( + + ), + }, + { + title: 'Author', + field: 'author', + width: '20%', + render: ({ author }) => , }, ]; @@ -77,6 +84,7 @@ export const TodoList = () => { options={{ search: false, pageSize, + padding: 'dense', }} page={page} columns={columns} From 3a7a803b0b44288772bf6db66306b5fac67ff15a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 01:34:56 +0100 Subject: [PATCH 18/42] todo-backend: filter out all dotfiles Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 919cf3dd18..7979b8f18e 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -99,7 +99,7 @@ export class TodoScmReader implements TodoReader { const tree = await this.reader.readTree(url, { etag, filter(path) { - return !path.startsWith('.yarn'); + return !path.startsWith('.') && !path.includes('/.'); }, }); From 803cc34d47406a3fe62ae3db649dfcd3d3c2eb7d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 21:46:47 +0100 Subject: [PATCH 19/42] todo-backend: add ordering support Signed-off-by: Patrik Oldsberg --- .../src/service/TodoReaderService.ts | 22 ++++++++- plugins/todo-backend/src/service/router.ts | 48 ++++++++++++++++++- plugins/todo-backend/src/service/types.ts | 4 ++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 57010ee7e0..85949065e7 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -78,8 +78,28 @@ export class TodoReaderService implements TodoService { offset = totalCount - limit; } + let items = todos.items; + const { orderBy } = req; + if (orderBy) { + const dir = orderBy.direction === 'asc' ? 1 : -1; + const field = orderBy.field; + items = items.slice().sort((item1, item2) => { + const field1 = item1[field]; + const field2 = item2[field]; + + if (field1 && field2) { + return dir * field1?.localeCompare(field2, 'en-US'); + } else if (field1 && !field2) { + return -1; + } else if (!field1 && field2) { + return 1; + } + return 0; + }); + } + return { - items: todos.items.slice(offset, offset + limit), + items: items.slice(offset, offset + limit), totalCount, offset, limit, diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index 12655cb208..1132b879c4 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -20,6 +20,13 @@ import express from 'express'; import Router from 'express-promise-router'; import { TodoService } from './types'; +const ALLOWED_ORDER_BY_FIELDS = [ + 'text', + 'author', + 'viewUrl', + 'repoFilePath', +] as const; + export interface RouterOptions { todoService: TodoService; } @@ -35,6 +42,11 @@ export async function createRouter( router.get('/v1/todos', async (req, res) => { const offset = parseIntegerParam(req.query.offset, 'offset query'); const limit = parseIntegerParam(req.query.limit, 'limit query'); + const orderBy = parseOrderByParam( + req.query.orderBy, + 'orderBy query', + ALLOWED_ORDER_BY_FIELDS, + ); const entityRef = req.query.entity; if (entityRef && typeof entityRef !== 'string') { @@ -49,7 +61,12 @@ export async function createRouter( } } - const todos = await todoService.listTodos({ entity, offset, limit }); + const todos = await todoService.listTodos({ + entity, + offset, + limit, + orderBy, + }); res.json(todos); }); @@ -69,3 +86,32 @@ function parseIntegerParam(str: unknown, ctx: string): number | undefined { } return parsed; } + +function parseOrderByParam( + str: unknown, + ctx: string, + allowedFields: T, +): { field: T[number]; direction: 'asc' | 'desc' } | undefined { + if (str === undefined) { + return undefined; + } + if (typeof str !== 'string') { + throw new InputError(`invalid ${ctx}, must be a string`); + } + const [field, direction] = str.split('='); + if (!field) { + throw new InputError(`invalid ${ctx}, field name is empty`); + } + if (direction !== 'asc' && direction !== 'desc') { + throw new InputError( + `invalid ${ctx}, order direction must be 'asc' or 'desc'`, + ); + } + + if (field && !allowedFields.includes(field)) { + throw new InputError( + `invalid orderBy query, must be one of ${allowedFields.join(', ')}`, + ); + } + return { field, direction }; +} diff --git a/plugins/todo-backend/src/service/types.ts b/plugins/todo-backend/src/service/types.ts index ac8ecc16b5..0f06951132 100644 --- a/plugins/todo-backend/src/service/types.ts +++ b/plugins/todo-backend/src/service/types.ts @@ -21,6 +21,10 @@ export type ListTodosRequest = { entity?: EntityName; offset?: number; limit?: number; + orderBy?: { + field: 'text' | 'author' | 'viewUrl' | 'repoFilePath'; + direction: 'asc' | 'desc'; + }; }; export type ListTodosResponse = { From bdf5d7de5905a44d26bfc69e6b90b53a00a20645 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 22:02:19 +0100 Subject: [PATCH 20/42] todo: added support for order in api and table Signed-off-by: Patrik Oldsberg --- plugins/todo/src/api/TodoClient.ts | 4 ++ plugins/todo/src/api/types.ts | 4 ++ .../todo/src/components/TodoList/TodoList.tsx | 63 +++++++++++-------- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index 44d80ba22e..ca5284537b 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -36,6 +36,7 @@ export class TodoClient implements TodoApi { entity, offset, limit, + orderBy, }: TodoListOptions): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('todo'); const token = await this.identityApi.getIdToken(); @@ -50,6 +51,9 @@ export class TodoClient implements TodoApi { if (typeof limit === 'number') { query.set('limit', String(limit)); } + if (orderBy) { + query.set('orderBy', `${orderBy.field}=${orderBy.direction}`); + } const res = await fetch(`${baseUrl}/v1/todos?${query}`, { headers: token diff --git a/plugins/todo/src/api/types.ts b/plugins/todo/src/api/types.ts index 759df24b60..22eff72b3a 100644 --- a/plugins/todo/src/api/types.ts +++ b/plugins/todo/src/api/types.ts @@ -38,6 +38,10 @@ export type TodoListOptions = { entity?: Entity; offset?: number; limit?: number; + orderBy?: { + field: 'text' | 'author' | 'viewUrl' | 'repoFilePath'; + direction: 'asc' | 'desc'; + }; }; export type TodoListResult = { diff --git a/plugins/todo/src/components/TodoList/TodoList.tsx b/plugins/todo/src/components/TodoList/TodoList.tsx index 1201dca4fd..a18e8f8626 100644 --- a/plugins/todo/src/components/TodoList/TodoList.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.tsx @@ -15,7 +15,6 @@ */ import { - Progress, Table, TableColumn, useApi, @@ -25,19 +24,22 @@ import { import { useEntity } from '@backstage/plugin-catalog-react'; import Alert from '@material-ui/lab/Alert'; import React, { useState } from 'react'; -import { useAsync } from 'react-use'; import { todoApiRef } from '../../api'; -import { TodoItem } from '../../api/types'; +import { TodoItem, TodoListOptions } from '../../api/types'; + +const PAGE_SIZE = 10; const columns: TableColumn[] = [ { title: 'Text', + field: 'text', width: '100%', highlight: true, render: ({ text }) => , }, { title: 'File', + field: 'repoFilePath', width: '80%', render: ({ viewUrl, repoFilePath }) => viewUrl ? ( @@ -59,39 +61,50 @@ const columns: TableColumn[] = [ export const TodoList = () => { const { entity } = useEntity(); const todoApi = useApi(todoApiRef); - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(10); + const [error, setError] = useState(); - const { value, loading, error } = useAsync( - async () => - todoApi.listTodos({ - entity, - offset: page * pageSize, - limit: pageSize, - }), - [todoApi, entity, page, pageSize], - ); - - if (loading) { - return ; - } else if (error) { + if (error) { return {error.message}; } return ( -
title="TODOs" options={{ search: false, - pageSize, + pageSize: PAGE_SIZE, padding: 'dense', + sorting: true, + draggable: false, + paging: true, + paginationType: 'stepped', }} - page={page} columns={columns} - totalCount={value!.totalCount} - onChangePage={setPage} - onChangeRowsPerPage={setPageSize} - data={value!.items} + data={async query => { + try { + const page = query?.page ?? 0; + const pageSize = query?.pageSize ?? PAGE_SIZE; + const result = await todoApi.listTodos({ + entity, + offset: page * pageSize, + limit: pageSize, + orderBy: + query?.orderBy && + ({ + field: query.orderBy.field, + direction: query.orderDirection, + } as TodoListOptions['orderBy']), + }); + return { + data: result.items, + totalCount: result.totalCount, + page: Math.floor(result.offset / result.limit), + }; + } catch (loadingError) { + setError(loadingError); + return { data: [], totalCount: 0, page: 0 }; + } + }} /> ); }; From 4fb393e98f8fde16302b3b0792749b196274a436 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 22:35:31 +0100 Subject: [PATCH 21/42] todo-backend: use integrations to resolve line numbers Signed-off-by: Patrik Oldsberg --- .../todo-backend/src/lib/TodoReader/TodoScmReader.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 7979b8f18e..316ac7840f 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -114,10 +114,6 @@ export class TodoScmReader implements TodoReader { path: file.path, content: content.toString('utf8'), }); - const viewUrl = this.integrations.resolveUrl({ - url: file.path, - base: url, - }); todos.push( ...items.map(({ lineNumber, text, author }) => ({ @@ -125,8 +121,11 @@ export class TodoScmReader implements TodoReader { author, lineNumber, repoFilePath: file.path, - viewUrl: - lineNumber === undefined ? viewUrl : `${viewUrl}#L${lineNumber}`, + viewUrl: this.integrations.resolveUrl({ + url: file.path, + base: url, + lineNumber, + }), })), ); } catch (error) { From c44db42c849a101eafdc50944be99a9920b27eae Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 23:12:16 +0100 Subject: [PATCH 22/42] todo,todo-backend: updated READMEs Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/README.md | 40 +++++++++++++++++++++++++++------- plugins/todo/README.md | 16 +++++--------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/plugins/todo-backend/README.md b/plugins/todo-backend/README.md index 78da07c683..4f79bab7a8 100644 --- a/plugins/todo-backend/README.md +++ b/plugins/todo-backend/README.md @@ -1,13 +1,37 @@ -# todo +# @backstage/plugin-todo-backend -Welcome to the todo backend plugin! +Backend for the `@backstage/plugin-todo` plugin. Assists in scanning for and listing `// TODO` comments in source code repositories. -_This plugin was created through the Backstage CLI_ +## Installation -## Getting started +Install the `@backstage/plugin-todo-backend` package in your backend packages, and then integrate the plugin using the following default setup for `src/plugins/todo.ts`: -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/todo](http://localhost:3000/todo). +```ts +import { Router } from 'express'; +import { CatalogClient } from '@backstage/catalog-client'; +import { + createRouter, + TodoReaderService, + TodoScmReader, +} from '@backstage/plugin-todo-backend'; +import { PluginEnvironment } from '../types'; -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. +export default async function createPlugin({ + logger, + reader, + config, + discovery, +}: PluginEnvironment): Promise { + const todoReader = TodoScmReader.fromConfig(config, { + logger, + reader, + }); + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const todoService = new TodoReaderService({ + todoReader, + catalogClient, + }); + + return await createRouter({ todoService }); +} +``` diff --git a/plugins/todo/README.md b/plugins/todo/README.md index a74a5c351b..aeaa72b3b9 100644 --- a/plugins/todo/README.md +++ b/plugins/todo/README.md @@ -1,13 +1,9 @@ -# todo +# @backstage/plugin-todo -Welcome to the todo plugin! +This plugins lists `// TODO` comments in source code. It currently exports a single component extension for use on entity pages. -_This plugin was created through the Backstage CLI_ +## Extensions -## Getting started - -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/todo](http://localhost:3000/todo). - -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. +| name | description | +| ------------------- | ------------------------------------------------------------------------------- | +| `EntityTodoContent` | Content for an entity page, showing a table of TODO items for the given entity. | From 38201c6ec4a47bb5d1fc5d0145158809b5066aa0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 23:13:11 +0100 Subject: [PATCH 23/42] todo,todo-backend: added repo info Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/package.json | 6 ++++++ plugins/todo/package.json | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 9fd5f68948..58d335732b 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -9,6 +9,12 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/todo-backend" + }, "scripts": { "build": "backstage-cli backend:build", "lint": "backstage-cli lint", diff --git a/plugins/todo/package.json b/plugins/todo/package.json index de51fe2f18..ee105287f5 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -9,6 +9,12 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/todo" + }, "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", From 2f6faae4641f6e5b5ca43f2789a04bc5e6971a8a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 23:13:29 +0100 Subject: [PATCH 24/42] todo: avoid routable extension for now Signed-off-by: Patrik Oldsberg --- plugins/todo/src/plugin.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/plugins/todo/src/plugin.ts b/plugins/todo/src/plugin.ts index 535630ba35..6dc06a7c34 100644 --- a/plugins/todo/src/plugin.ts +++ b/plugins/todo/src/plugin.ts @@ -16,13 +16,13 @@ import { createApiFactory, createPlugin, - createRoutableExtension, + createComponentExtension, discoveryApiRef, identityApiRef, } from '@backstage/core'; import { todoApiRef, TodoClient } from './api'; -import { rootRouteRef } from './routes'; +// import { rootRouteRef } from './routes'; export const todoPlugin = createPlugin({ id: 'todo', @@ -39,13 +39,16 @@ export const todoPlugin = createPlugin({ }), ], routes: { - root: rootRouteRef, + // root: rootRouteRef, }, }); export const EntityTodoContent = todoPlugin.provide( - createRoutableExtension({ - component: () => import('./components/TodoList').then(m => m.TodoList), - mountPoint: rootRouteRef, + createComponentExtension({ + component: { + lazy: () => import('./components/TodoList').then(m => m.TodoList), + }, + // TODO(Rugvip): Switch back to routable extension once apps are migrated + // mountPoint: rootRouteRef, }), ); From a7648a37fafd1ed8b52489154ee6c0597934467c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 23:13:56 +0100 Subject: [PATCH 25/42] app: add todo plugin Signed-off-by: Patrik Oldsberg --- packages/app/package.json | 1 + .../app/src/components/catalog/EntityPage.tsx | 16 ++++++++++++++++ packages/app/src/plugins.ts | 1 + 3 files changed, 18 insertions(+) diff --git a/packages/app/package.json b/packages/app/package.json index 21f0d0c3d0..7925c203e3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -33,6 +33,7 @@ "@backstage/plugin-search": "^0.3.3", "@backstage/plugin-tech-radar": "^0.3.7", "@backstage/plugin-techdocs": "^0.6.1", + "@backstage/plugin-todo": "^0.1.1", "@backstage/plugin-user-settings": "^0.2.7", "@backstage/theme": "^0.2.4", "@material-ui/core": "^4.11.0", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index fb56b40e68..d8883e7d18 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -81,6 +81,7 @@ import { } from '@backstage/plugin-rollbar'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; +import { EntityTodoContent } from '@backstage/plugin-todo'; import { Button, Grid } from '@material-ui/core'; import { isPluginApplicableToEntity as isBuildkiteAvailable, @@ -278,6 +279,11 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( title="Kafka" element={} /> + } + /> ); @@ -323,6 +329,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( title="Code Insights" element={} /> + } + /> ); @@ -338,6 +349,11 @@ const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( title="Docs" element={} /> + } + /> ); diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 862d821ed6..29d19785bf 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -43,3 +43,4 @@ export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite'; export { plugin as Search } from '@backstage/plugin-search'; export { plugin as Org } from '@backstage/plugin-org'; export { plugin as Kafka } from '@backstage/plugin-kafka'; +export { todoPlugin } from '@backstage/plugin-todo'; From 9c4ebfb4e48ba10503b8923f0cd1698b959b9492 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Mar 2021 01:18:46 +0100 Subject: [PATCH 26/42] todo: update TodoList test Signed-off-by: Patrik Oldsberg --- .../todo/src/components/TodoList/TodoList.test.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/todo/src/components/TodoList/TodoList.test.tsx b/plugins/todo/src/components/TodoList/TodoList.test.tsx index 2f44b36673..bd90f40215 100644 --- a/plugins/todo/src/components/TodoList/TodoList.test.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.test.tsx @@ -26,9 +26,16 @@ describe('TodoList', () => { it('should render', async () => { const mockApi: jest.Mocked = { listTodos: jest.fn().mockResolvedValue({ - items: [{ text: 'My TODO' }], + items: [ + { + text: 'My TODO', + viewUrl: 'https://example.com', + repoFilePath: '/my-file.js', + }, + ], totalCount: 1, - cursors: { prev: 'prev', self: 'self', next: 'next' }, + limit: 10, + offset: 0, }), }; const mockEntity = { metadata: { name: 'mock' } } as Entity; @@ -41,6 +48,6 @@ describe('TodoList', () => { , ); - await expect(rendered.findByText('My TODO')).resolves.toBeInTheDocument(); + await expect(rendered.findByText('1-1 of 1')).resolves.toBeInTheDocument(); }); }); From 0d7db68595813ae97c8e842a67f2e37531414dcb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Mar 2021 17:32:59 +0100 Subject: [PATCH 27/42] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Adam Harvey Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts | 4 +++- plugins/todo-backend/src/service/TodoReaderService.ts | 2 +- plugins/todo/README.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 316ac7840f..2a191d2cb4 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -129,7 +129,9 @@ export class TodoScmReader implements TodoReader { })), ); } catch (error) { - this.logger.error(`Failed to parse TODO in ${url}, ${error}`); + this.logger.error( + `Failed to parse TODO in ${url} at ${file.path}, ${error}`, + ); } } diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 85949065e7..3965257eff 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -51,7 +51,7 @@ export class TodoReaderService implements TodoService { async listTodos(req: ListTodosRequest): Promise { if (!req.entity) { - throw new InputError('entity filter is required to list todos'); + throw new InputError('Entity filter is required to list TODOs'); } const entity = await this.catalogClient.getEntityByName(req.entity); if (!entity) { diff --git a/plugins/todo/README.md b/plugins/todo/README.md index aeaa72b3b9..8a8383b5f7 100644 --- a/plugins/todo/README.md +++ b/plugins/todo/README.md @@ -1,6 +1,6 @@ # @backstage/plugin-todo -This plugins lists `// TODO` comments in source code. It currently exports a single component extension for use on entity pages. +This plugin lists `// TODO` comments in source code. It currently exports a single component extension for use on entity pages. ## Extensions From e1d3fcf1c016321ea1e4c3f966870cf17340df4d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Mar 2021 17:50:29 +0100 Subject: [PATCH 28/42] todo-backend: review suggestions Signed-off-by: Patrik Oldsberg --- packages/app/package.json | 2 +- plugins/todo-backend/package.json | 2 +- plugins/todo-backend/src/service/TodoReaderService.ts | 2 -- plugins/todo-backend/src/service/router.test.ts | 2 +- plugins/todo/package.json | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 7925c203e3..3d9f83d89f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -33,7 +33,7 @@ "@backstage/plugin-search": "^0.3.3", "@backstage/plugin-tech-radar": "^0.3.7", "@backstage/plugin-techdocs": "^0.6.1", - "@backstage/plugin-todo": "^0.1.1", + "@backstage/plugin-todo": "^0.1.0", "@backstage/plugin-user-settings": "^0.2.7", "@backstage/theme": "^0.2.4", "@material-ui/core": "^4.11.0", diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 58d335732b..888c9a404c 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-todo-backend", - "version": "0.1.1", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 3965257eff..bc8ebbcd22 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -74,8 +74,6 @@ export class TodoReaderService implements TodoService { let offset = req.offset ?? 0; if (offset < 0) { offset = 0; - } else if (offset - limit > totalCount) { - offset = totalCount - limit; } let items = todos.items; diff --git a/plugins/todo-backend/src/service/router.test.ts b/plugins/todo-backend/src/service/router.test.ts index 1bd70c1643..3c17176dab 100644 --- a/plugins/todo-backend/src/service/router.test.ts +++ b/plugins/todo-backend/src/service/router.test.ts @@ -44,7 +44,7 @@ describe('createRouter', () => { jest.resetAllMocks(); }); - describe('GET /health', () => { + describe('GET /todos', () => { it('returns list without query', async () => { mockService.listTodos.mockResolvedValueOnce(mockListBody); diff --git a/plugins/todo/package.json b/plugins/todo/package.json index ee105287f5..d940dfbe8a 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-todo", - "version": "0.1.1", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 51da6891e0ee718e940aadff82b66725f235954e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Mar 2021 18:14:02 +0100 Subject: [PATCH 29/42] todo,todo-backend: use @backstage/errors Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/package.json | 1 + .../src/service/TodoReaderService.ts | 2 +- plugins/todo-backend/src/service/router.ts | 2 +- plugins/todo/package.json | 1 + plugins/todo/src/api/TodoClient.ts | 32 ++----------------- .../todo/src/components/TodoList/TodoList.tsx | 4 +-- 6 files changed, 8 insertions(+), 34 deletions(-) diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 888c9a404c..fd9bc46e1e 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -28,6 +28,7 @@ "@backstage/catalog-client": "^0.3.6", "@backstage/catalog-model": "^0.7.3", "@backstage/config": "^0.1.3", + "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.0", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index bc8ebbcd22..66ac0dbf4c 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { InputError, NotFoundError } from '@backstage/backend-common'; +import { InputError, NotFoundError } from '@backstage/errors'; import { CatalogClient } from '@backstage/catalog-client'; import { LOCATION_ANNOTATION, diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index 1132b879c4..d4f1a49c70 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { InputError } from '@backstage/backend-common'; import { EntityName, parseEntityName } from '@backstage/catalog-model'; +import { InputError } from '@backstage/errors'; import express from 'express'; import Router from 'express-promise-router'; import { TodoService } from './types'; diff --git a/plugins/todo/package.json b/plugins/todo/package.json index d940dfbe8a..91802b7b4a 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -28,6 +28,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.3", "@backstage/core": "^0.7.0", + "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index ca5284537b..07b5eabead 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -15,6 +15,7 @@ */ import { serializeEntityRef } from '@backstage/catalog-model'; +import { ResponseError } from '@backstage/errors'; import { DiscoveryApi, IdentityApi } from '@backstage/core'; import { TodoApi, TodoListOptions, TodoListResult } from './types'; @@ -64,39 +65,10 @@ export class TodoClient implements TodoApi { }); if (!res.ok) { - const error = await this.readResponseError(res, 'list todos'); - throw error; + throw await ResponseError.fromResponse(res); } const data: TodoListResult = await res.json(); return data; } - - private async readResponseError(res: Response, action: string) { - const error = new Error() as Error & { status: number }; - error.status = res.status; - - try { - const json = await res.clone().json(); - if (typeof json?.error?.message !== 'string') { - throw new Error('invalid error'); - } - error.message = json.error.message; - if (json.error.name) { - error.name = json.error.name; - } - } catch { - try { - const text = await res.text(); - error.message = `Failed to ${action}, ${text}`; - } catch { - error.message = `Failed to ${action}, status ${res.status}`; - } - if (res.status === 404) { - error.name = 'NotFoundError'; - } - } - - throw error; - } } diff --git a/plugins/todo/src/components/TodoList/TodoList.tsx b/plugins/todo/src/components/TodoList/TodoList.tsx index a18e8f8626..999aa4fcbe 100644 --- a/plugins/todo/src/components/TodoList/TodoList.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.tsx @@ -20,9 +20,9 @@ import { useApi, OverflowTooltip, Link, + ResponseErrorPanel, } from '@backstage/core'; import { useEntity } from '@backstage/plugin-catalog-react'; -import Alert from '@material-ui/lab/Alert'; import React, { useState } from 'react'; import { todoApiRef } from '../../api'; import { TodoItem, TodoListOptions } from '../../api/types'; @@ -64,7 +64,7 @@ export const TodoList = () => { const [error, setError] = useState(); if (error) { - return {error.message}; + return ; } return ( From ebeb76822a95cbb9612be8c7aaafe9421d3bec40 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Mar 2021 20:31:18 +0100 Subject: [PATCH 30/42] todo: sync package versions Signed-off-by: Patrik Oldsberg --- plugins/todo/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 91802b7b4a..36faa1d9c0 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -27,10 +27,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.3", - "@backstage/core": "^0.7.0", + "@backstage/core": "^0.7.1", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.1", - "@backstage/theme": "^0.2.3", + "@backstage/theme": "^0.2.4", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -39,7 +39,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.3", + "@backstage/cli": "^0.6.4", "@backstage/dev-utils": "^0.1.13", "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", From fd4547221311fa2db5cc0e9fb37b513f4f8d69fe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Mar 2021 21:23:17 +0100 Subject: [PATCH 31/42] todo-backend: add support for configuring the default parser Signed-off-by: Patrik Oldsberg --- .github/styles/vocab.txt | 1 + plugins/todo-backend/README.md | 21 +++++++++ .../src/lib/TodoReader/TodoScmReader.ts | 28 ++---------- .../src/lib/TodoReader/createTodoParser.ts | 44 +++++++++++++++++++ .../todo-backend/src/lib/TodoReader/index.ts | 1 + .../todo-backend/src/lib/TodoReader/types.ts | 13 ++++++ 6 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 489727ca4d..dc3ef0f036 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -42,6 +42,7 @@ JavaScript Kaewkasi Knex Kumar +Leasot Lerna Lundberg Luxon diff --git a/plugins/todo-backend/README.md b/plugins/todo-backend/README.md index 4f79bab7a8..0e455977f9 100644 --- a/plugins/todo-backend/README.md +++ b/plugins/todo-backend/README.md @@ -35,3 +35,24 @@ export default async function createPlugin({ return await createRouter({ todoService }); } ``` + +## Parser Configuration + +The `TodoScmReader` accepts a `TodoParser` option, which can be used to configure your own parser. The default one is based on [Leasot](https://github.com/pgilad/leasot) and supports a wide range of languages. You can change the list of supported tags by configuring your own version of the built-in parser, for example: + +```ts +import { + TodoScmReader, + createTodoParser, +} from '@backstage/plugin-todo-backend'; + +// ... + +const todoReader = TodoScmReader.fromConfig(config, { + logger, + reader, + parser: createTodoParser({ + tags: ['TODO', 'FIXME', 'NOTE', 'XXX'], + }), +}); +``` diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 2a191d2cb4..302c5985d6 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -14,23 +14,19 @@ * limitations under the License. */ -import { extname } from 'path'; import { UrlReader } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { Logger } from 'winston'; -import { parse } from 'leasot'; + import { ReadTodosOptions, ReadTodosResult, TodoItem, + TodoParser, TodoReader, } from './types'; import { Config } from '@backstage/config'; - -type TodoParser = (ctx: { - content: string; - path: string; -}) => { text: string; author?: string; lineNumber: number }[]; +import { createTodoParser } from './createTodoParser'; type Options = { logger: Logger; @@ -43,22 +39,6 @@ type CacheItem = { result: ReadTodosResult; }; -const defaultTodoParser: TodoParser = ({ content, path }) => { - try { - const comments = parse(content, { - extension: extname(path), - }); - - return comments.map(comment => ({ - text: comment.text, - author: comment.ref, - lineNumber: comment.line, - })); - } catch /* ignore unsupported extensions */ { - return []; - } -}; - export class TodoScmReader implements TodoReader { private readonly logger: Logger; private readonly reader: UrlReader; @@ -74,7 +54,7 @@ export class TodoScmReader implements TodoReader { private constructor(options: Options, integrations: ScmIntegrations) { this.logger = options.logger; this.reader = options.reader; - this.parser = options.parser ?? defaultTodoParser; + this.parser = options.parser ?? createTodoParser(); this.integrations = integrations; } diff --git a/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts b/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts new file mode 100644 index 0000000000..deb0ed8800 --- /dev/null +++ b/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { extname } from 'path'; +import { parse } from 'leasot'; +import { TodoParser } from './types'; + +export type TodoParserOptions = { + tags?: string[]; +}; + +export function createTodoParser(options: TodoParserOptions = {}): TodoParser { + const { tags = ['TODO', 'FIXME'] } = options; + + return ({ content, path }) => { + try { + const comments = parse(content, { + customTags: tags, + extension: extname(path), + }); + + return comments.map(comment => ({ + text: comment.text, + author: comment.ref, + lineNumber: comment.line, + })); + } catch /* ignore unsupported extensions */ { + return []; + } + }; +} diff --git a/plugins/todo-backend/src/lib/TodoReader/index.ts b/plugins/todo-backend/src/lib/TodoReader/index.ts index 22c47ebef2..58982385c8 100644 --- a/plugins/todo-backend/src/lib/TodoReader/index.ts +++ b/plugins/todo-backend/src/lib/TodoReader/index.ts @@ -15,6 +15,7 @@ */ export { TodoScmReader } from './TodoScmReader'; +export { createTodoParser } from './createTodoParser'; export type { TodoItem, TodoReader, diff --git a/plugins/todo-backend/src/lib/TodoReader/types.ts b/plugins/todo-backend/src/lib/TodoReader/types.ts index 499d3ac843..78859841d7 100644 --- a/plugins/todo-backend/src/lib/TodoReader/types.ts +++ b/plugins/todo-backend/src/lib/TodoReader/types.ts @@ -51,3 +51,16 @@ export interface TodoReader { */ readTodos(options: ReadTodosOptions): Promise; } + +type TodoParserContext = { + content: string; + path: string; +}; + +type TodoParserResult = { + text: string; + author?: string; + lineNumber: number; +}; + +export type TodoParser = (ctx: TodoParserContext) => TodoParserResult[]; From 378f77b4f63db52653c7f5a49eed81d87001c5ce Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Mar 2021 21:32:46 +0100 Subject: [PATCH 32/42] todo: add format intro and examples to README Signed-off-by: Patrik Oldsberg --- plugins/todo/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/plugins/todo/README.md b/plugins/todo/README.md index 8a8383b5f7..4165582b65 100644 --- a/plugins/todo/README.md +++ b/plugins/todo/README.md @@ -2,6 +2,32 @@ This plugin lists `// TODO` comments in source code. It currently exports a single component extension for use on entity pages. +## Format + +The default parser uses [Leasot](https://github.com/pgilad/leasot), which supports a wide range of languages. By default it supports the `TODO` and `FIXME` tags, along with `@` prefix and author reference through with either a `()` suffix or trailing `/`. For more information on how to configure the parser, see `@backstage/plugin-todo-backend`. + +Below are some examples of formats that are supported by default: + +```ts +// TODO: Ideally this would be working + +// TODO(Rugvip): Not sure why this works, investigate + +// @todo: This worked last Monday /Rugvip + +// FIXME Nobody knows why this is here +``` + +Note the trailing comments are not supported, the following TODO would not be listed: + +```ts +function reverse(str: string) { + return str.reverse(); // TODO: optimize +} +``` + +The scanner also ignores all dot-files and directories, meaning TODOs inside of those will not be listed. + ## Extensions | name | description | From c07728eec8b3441e398389a3cb8942d0faea61a7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Mar 2021 22:10:47 +0100 Subject: [PATCH 33/42] todo,todo-backend: add tag field to TodoItems Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts | 3 ++- plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts | 1 + plugins/todo-backend/src/lib/TodoReader/types.ts | 4 ++++ plugins/todo-backend/src/service/router.test.ts | 2 +- plugins/todo-backend/src/service/router.ts | 1 + plugins/todo-backend/src/service/types.ts | 2 +- plugins/todo/dev/index.tsx | 1 + plugins/todo/src/api/types.ts | 5 ++++- plugins/todo/src/components/TodoList/TodoList.test.tsx | 3 ++- plugins/todo/src/components/TodoList/TodoList.tsx | 5 ++++- 10 files changed, 21 insertions(+), 6 deletions(-) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 302c5985d6..07ebdf97eb 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -96,8 +96,9 @@ export class TodoScmReader implements TodoReader { }); todos.push( - ...items.map(({ lineNumber, text, author }) => ({ + ...items.map(({ lineNumber, text, tag, author }) => ({ text, + tag, author, lineNumber, repoFilePath: file.path, diff --git a/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts b/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts index deb0ed8800..1abbb901db 100644 --- a/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts +++ b/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts @@ -34,6 +34,7 @@ export function createTodoParser(options: TodoParserOptions = {}): TodoParser { return comments.map(comment => ({ text: comment.text, + tag: comment.tag, author: comment.ref, lineNumber: comment.line, })); diff --git a/plugins/todo-backend/src/lib/TodoReader/types.ts b/plugins/todo-backend/src/lib/TodoReader/types.ts index 78859841d7..a6f046d2ea 100644 --- a/plugins/todo-backend/src/lib/TodoReader/types.ts +++ b/plugins/todo-backend/src/lib/TodoReader/types.ts @@ -18,6 +18,9 @@ export type TodoItem = { /** The contents of the TODO comment */ text: string; + /** The tag used, e.g. TODO, FIXME */ + tag: string; + /** References author, if any */ author?: string; @@ -59,6 +62,7 @@ type TodoParserContext = { type TodoParserResult = { text: string; + tag: string; author?: string; lineNumber: number; }; diff --git a/plugins/todo-backend/src/service/router.test.ts b/plugins/todo-backend/src/service/router.test.ts index 3c17176dab..8fda603937 100644 --- a/plugins/todo-backend/src/service/router.test.ts +++ b/plugins/todo-backend/src/service/router.test.ts @@ -21,7 +21,7 @@ import { createRouter } from './router'; import { TodoService } from './types'; const mockListBody = { - items: [{ text: 'my todo' }], + items: [{ text: 'my todo', tag: 'TODO' }], totalCount: 1, offset: 0, limit: 10, diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index d4f1a49c70..7b73ff93af 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -22,6 +22,7 @@ import { TodoService } from './types'; const ALLOWED_ORDER_BY_FIELDS = [ 'text', + 'tag', 'author', 'viewUrl', 'repoFilePath', diff --git a/plugins/todo-backend/src/service/types.ts b/plugins/todo-backend/src/service/types.ts index 0f06951132..e37307a4cf 100644 --- a/plugins/todo-backend/src/service/types.ts +++ b/plugins/todo-backend/src/service/types.ts @@ -22,7 +22,7 @@ export type ListTodosRequest = { offset?: number; limit?: number; orderBy?: { - field: 'text' | 'author' | 'viewUrl' | 'repoFilePath'; + field: 'text' | 'tag' | 'author' | 'viewUrl' | 'repoFilePath'; direction: 'asc' | 'desc'; }; }; diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx index 25a6769cb5..71e5e36355 100644 --- a/plugins/todo/dev/index.tsx +++ b/plugins/todo/dev/index.tsx @@ -50,6 +50,7 @@ const mockedApi = { items: [ { text: 'Make sure this works', + tag: 'TODO', author: 'Rugvip', viewUrl: 'https://github.com/backstage/backstage', }, diff --git a/plugins/todo/src/api/types.ts b/plugins/todo/src/api/types.ts index 22eff72b3a..e615532113 100644 --- a/plugins/todo/src/api/types.ts +++ b/plugins/todo/src/api/types.ts @@ -21,6 +21,9 @@ export type TodoItem = { /** The contents of the TODO comment */ text: string; + /** The tag used, e.g. TODO, FIXME */ + tag: string; + /** References author, if any */ author?: string; @@ -39,7 +42,7 @@ export type TodoListOptions = { offset?: number; limit?: number; orderBy?: { - field: 'text' | 'author' | 'viewUrl' | 'repoFilePath'; + field: 'text' | 'tag' | 'author' | 'viewUrl' | 'repoFilePath'; direction: 'asc' | 'desc'; }; }; diff --git a/plugins/todo/src/components/TodoList/TodoList.test.tsx b/plugins/todo/src/components/TodoList/TodoList.test.tsx index bd90f40215..2fb4f780bc 100644 --- a/plugins/todo/src/components/TodoList/TodoList.test.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.test.tsx @@ -29,6 +29,7 @@ describe('TodoList', () => { items: [ { text: 'My TODO', + tag: 'FIXME', viewUrl: 'https://example.com', repoFilePath: '/my-file.js', }, @@ -48,6 +49,6 @@ describe('TodoList', () => { , ); - await expect(rendered.findByText('1-1 of 1')).resolves.toBeInTheDocument(); + await expect(rendered.findByText('FIXME')).resolves.toBeInTheDocument(); }); }); diff --git a/plugins/todo/src/components/TodoList/TodoList.tsx b/plugins/todo/src/components/TodoList/TodoList.tsx index 999aa4fcbe..2ef8fceeef 100644 --- a/plugins/todo/src/components/TodoList/TodoList.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.tsx @@ -30,6 +30,10 @@ import { TodoItem, TodoListOptions } from '../../api/types'; const PAGE_SIZE = 10; const columns: TableColumn[] = [ + { + title: 'Tag', + field: 'tag', + }, { title: 'Text', field: 'text', @@ -53,7 +57,6 @@ const columns: TableColumn[] = [ { title: 'Author', field: 'author', - width: '20%', render: ({ author }) => , }, ]; From 6231a9a0d4b06eb877322b1a00caa9d19caa5af6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Mar 2021 23:30:42 +0100 Subject: [PATCH 34/42] todo,todo-backend: add support for filtering Signed-off-by: Patrik Oldsberg --- .../src/service/TodoReaderService.ts | 20 ++++++- plugins/todo-backend/src/service/router.ts | 58 +++++++++++++++---- plugins/todo-backend/src/service/types.ts | 9 ++- plugins/todo/src/api/TodoClient.ts | 6 ++ plugins/todo/src/api/types.ts | 9 ++- .../todo/src/components/TodoList/TodoList.tsx | 10 +++- 6 files changed, 96 insertions(+), 16 deletions(-) diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 66ac0dbf4c..994866db93 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -36,6 +36,13 @@ type Options = { defaultPageSize?: number; }; +function wildcardRegex(str: string): RegExp { + const pattern = str + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*'); + return new RegExp(`^${pattern}$`, 'i'); +} + export class TodoReaderService implements TodoService { private readonly todoReader: TodoReader; private readonly catalogClient: CatalogClient; @@ -62,7 +69,6 @@ export class TodoReaderService implements TodoService { const url = this.getEntitySourceUrl(entity); const todos = await this.todoReader.readTodos({ url }); - const totalCount = todos.items.length; let limit = req.limit ?? this.defaultPageSize; if (limit < 0) { @@ -77,7 +83,15 @@ export class TodoReaderService implements TodoService { } let items = todos.items; - const { orderBy } = req; + const { orderBy, filters } = req; + + if (filters) { + for (const { field, value } of filters) { + const pattern = wildcardRegex(value); + items = items.filter(item => item[field]?.match(pattern)); + } + } + if (orderBy) { const dir = orderBy.direction === 'asc' ? 1 : -1; const field = orderBy.field; @@ -98,7 +112,7 @@ export class TodoReaderService implements TodoService { return { items: items.slice(offset, offset + limit), - totalCount, + totalCount: items.length, offset, limit, }; diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index 7b73ff93af..d2eb7278f8 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -20,7 +20,7 @@ import express from 'express'; import Router from 'express-promise-router'; import { TodoService } from './types'; -const ALLOWED_ORDER_BY_FIELDS = [ +const TODO_FIELDS = [ 'text', 'tag', 'author', @@ -43,11 +43,8 @@ export async function createRouter( router.get('/v1/todos', async (req, res) => { const offset = parseIntegerParam(req.query.offset, 'offset query'); const limit = parseIntegerParam(req.query.limit, 'limit query'); - const orderBy = parseOrderByParam( - req.query.orderBy, - 'orderBy query', - ALLOWED_ORDER_BY_FIELDS, - ); + const orderBy = parseOrderByParam(req.query.orderBy, TODO_FIELDS); + const filters = parseFilterParam(req.query.filter, TODO_FIELDS); const entityRef = req.query.entity; if (entityRef && typeof entityRef !== 'string') { @@ -67,6 +64,7 @@ export async function createRouter( offset, limit, orderBy, + filters, }); res.json(todos); }); @@ -90,22 +88,21 @@ function parseIntegerParam(str: unknown, ctx: string): number | undefined { function parseOrderByParam( str: unknown, - ctx: string, allowedFields: T, ): { field: T[number]; direction: 'asc' | 'desc' } | undefined { if (str === undefined) { return undefined; } if (typeof str !== 'string') { - throw new InputError(`invalid ${ctx}, must be a string`); + throw new InputError(`invalid orderBy query, must be a string`); } const [field, direction] = str.split('='); if (!field) { - throw new InputError(`invalid ${ctx}, field name is empty`); + throw new InputError(`invalid orderBy query, field name is empty`); } if (direction !== 'asc' && direction !== 'desc') { throw new InputError( - `invalid ${ctx}, order direction must be 'asc' or 'desc'`, + `invalid orderBy query, order direction must be 'asc' or 'desc'`, ); } @@ -116,3 +113,44 @@ function parseOrderByParam( } return { field, direction }; } + +function parseFilterParam( + str: unknown, + allowedFields: T, +): { field: T[number]; value: string }[] | undefined { + if (str === undefined) { + return undefined; + } + + const filters = new Array<{ field: T[number]; value: string }>(); + + const strs = [str].flat(); + for (const filterStr of strs) { + if (typeof filterStr !== 'string') { + throw new InputError( + `invalid filter query, must be a string or list of strings`, + ); + } + const splitIndex = filterStr.indexOf('='); + if (splitIndex <= 0) { + throw new InputError( + `invalid filter query, must separate field from value using '='`, + ); + } + + const field = filterStr.slice(0, splitIndex); + if (!allowedFields.includes(field)) { + throw new InputError( + `invalid filter field, must be one of ${allowedFields.join(', ')}`, + ); + } + + const value = filterStr.slice(splitIndex + 1); + if (!value) { + throw new InputError(`invalid filter query, value may not be empty`); + } + filters.push({ field, value }); + } + + return filters; +} diff --git a/plugins/todo-backend/src/service/types.ts b/plugins/todo-backend/src/service/types.ts index e37307a4cf..9216fe733d 100644 --- a/plugins/todo-backend/src/service/types.ts +++ b/plugins/todo-backend/src/service/types.ts @@ -17,14 +17,21 @@ import { EntityName } from '@backstage/catalog-model'; import { TodoItem } from '../lib'; +type Fields = 'text' | 'tag' | 'author' | 'viewUrl' | 'repoFilePath'; + export type ListTodosRequest = { entity?: EntityName; offset?: number; limit?: number; orderBy?: { - field: 'text' | 'tag' | 'author' | 'viewUrl' | 'repoFilePath'; + field: Fields; direction: 'asc' | 'desc'; }; + filters?: { + field: Fields; + /** Value to filter by, with '*' used as wildcard */ + value: string; + }[]; }; export type ListTodosResponse = { diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index 07b5eabead..9d9fff0144 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -38,6 +38,7 @@ export class TodoClient implements TodoApi { offset, limit, orderBy, + filters, }: TodoListOptions): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('todo'); const token = await this.identityApi.getIdToken(); @@ -55,6 +56,11 @@ export class TodoClient implements TodoApi { if (orderBy) { query.set('orderBy', `${orderBy.field}=${orderBy.direction}`); } + if (filters) { + for (const filter of filters) { + query.append('filter', `${filter.field}=${filter.value}`); + } + } const res = await fetch(`${baseUrl}/v1/todos?${query}`, { headers: token diff --git a/plugins/todo/src/api/types.ts b/plugins/todo/src/api/types.ts index e615532113..10c810592c 100644 --- a/plugins/todo/src/api/types.ts +++ b/plugins/todo/src/api/types.ts @@ -37,14 +37,21 @@ export type TodoItem = { repoFilePath?: string; }; +type Fields = 'text' | 'tag' | 'author' | 'viewUrl' | 'repoFilePath'; + export type TodoListOptions = { entity?: Entity; offset?: number; limit?: number; orderBy?: { - field: 'text' | 'tag' | 'author' | 'viewUrl' | 'repoFilePath'; + field: Fields; direction: 'asc' | 'desc'; }; + filters?: { + field: Fields; + /** Value to filter by, with '*' used as wildcard */ + value: string; + }[]; }; export type TodoListResult = { diff --git a/plugins/todo/src/components/TodoList/TodoList.tsx b/plugins/todo/src/components/TodoList/TodoList.tsx index 2ef8fceeef..101f1caac0 100644 --- a/plugins/todo/src/components/TodoList/TodoList.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.tsx @@ -33,6 +33,7 @@ const columns: TableColumn[] = [ { title: 'Tag', field: 'tag', + filtering: false, }, { title: 'Text', @@ -57,6 +58,7 @@ const columns: TableColumn[] = [ { title: 'Author', field: 'author', + width: '20%', render: ({ author }) => , }, ]; @@ -80,7 +82,9 @@ export const TodoList = () => { sorting: true, draggable: false, paging: true, - paginationType: 'stepped', + filtering: true, + debounceInterval: 500, + filterCellStyle: { padding: '0 16px 0 20px' }, }} columns={columns} data={async query => { @@ -97,6 +101,10 @@ export const TodoList = () => { field: query.orderBy.field, direction: query.orderDirection, } as TodoListOptions['orderBy']), + filters: query?.filters?.map(filter => ({ + field: filter.column.field!, + value: `*${filter.value}*`, + })) as TodoListOptions['filters'], }); return { data: result.items, From da6bce39e4465f07fe285ab4ef42deac204928ae Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Mar 2021 11:06:03 +0100 Subject: [PATCH 35/42] todo-backend: added comment parser tests and tweaks Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/README.md | 4 +- .../lib/TodoReader/createTodoParser.test.ts | 127 ++++++++++++++++++ .../src/lib/TodoReader/createTodoParser.ts | 9 +- 3 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 plugins/todo-backend/src/lib/TodoReader/createTodoParser.test.ts diff --git a/plugins/todo-backend/README.md b/plugins/todo-backend/README.md index 0e455977f9..e5f465466d 100644 --- a/plugins/todo-backend/README.md +++ b/plugins/todo-backend/README.md @@ -38,7 +38,7 @@ export default async function createPlugin({ ## Parser Configuration -The `TodoScmReader` accepts a `TodoParser` option, which can be used to configure your own parser. The default one is based on [Leasot](https://github.com/pgilad/leasot) and supports a wide range of languages. You can change the list of supported tags by configuring your own version of the built-in parser, for example: +The `TodoScmReader` accepts a `TodoParser` option, which can be used to configure your own parser. The default one is based on [Leasot](https://github.com/pgilad/leasot) and supports a wide range of languages. You can add to the list of supported tags by configuring your own version of the built-in parser, for example: ```ts import { @@ -52,7 +52,7 @@ const todoReader = TodoScmReader.fromConfig(config, { logger, reader, parser: createTodoParser({ - tags: ['TODO', 'FIXME', 'NOTE', 'XXX'], + additionalTags: ['NOTE', 'XXX'], }), }); ``` diff --git a/plugins/todo-backend/src/lib/TodoReader/createTodoParser.test.ts b/plugins/todo-backend/src/lib/TodoReader/createTodoParser.test.ts new file mode 100644 index 0000000000..0afe2c3420 --- /dev/null +++ b/plugins/todo-backend/src/lib/TodoReader/createTodoParser.test.ts @@ -0,0 +1,127 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { createTodoParser } from './createTodoParser'; + +const comment = '//'; // just so we don't have all of these should up as TODOs :D + +describe('createTodoParser', () => { + it('should create a parser that parses TODOs', () => { + const parser = createTodoParser(); + + const output = parser({ + path: 'my-file.js', + content: ` + ${comment} TODO(user1): todo 1 + ${comment}TODO(user2): todo 2 + ${comment}todo: todo 3 + ${comment}@todo: todo 4 + + ${comment}@fixme(user5): todo 5 + ${comment} @TODO: todo 6 /user6 + ${comment} FIXME: todo 7 /user7 + `, + }); + expect(output).toEqual([ + { + text: 'todo 1', + author: 'user1', + lineNumber: 2, + tag: 'TODO', + }, + { + text: 'todo 2', + author: 'user2', + lineNumber: 3, + tag: 'TODO', + }, + { + text: 'todo 3', + lineNumber: 4, + tag: 'TODO', + }, + { + text: 'todo 4', + lineNumber: 5, + tag: 'TODO', + }, + { + text: 'todo 5', + author: 'user5', + lineNumber: 7, + tag: 'FIXME', + }, + { + text: 'todo 6', + author: 'user6', + lineNumber: 8, + tag: 'TODO', + }, + { + text: 'todo 7', + author: 'user7', + lineNumber: 9, + tag: 'FIXME', + }, + ]); + }); + + it('should support custom tags', () => { + const content = ` + ${comment} TODO: todo 1 + ${comment} MYTAG: todo 2 + ${comment} MYTAG(user): todo 3 + `; + expect( + createTodoParser()({ + path: 'my-file.js', + content, + }), + ).toEqual([{ text: 'todo 1', tag: 'TODO', lineNumber: 2 }]); + expect( + createTodoParser({ additionalTags: ['MYTAG'] })({ + path: 'my-file.js', + content, + }), + ).toEqual([ + { text: 'todo 1', tag: 'TODO', lineNumber: 2 }, + { text: 'todo 2', tag: 'MYTAG', lineNumber: 3 }, + { text: 'todo 3', author: 'user', tag: 'MYTAG', lineNumber: 4 }, + ]); + }); + + it('should support multiple languages', () => { + const parser = createTodoParser(); + + const content = ` + -- TODO: todo 1 + # TODO: todo 2 + ${comment} TODO: todo 3 + ${comment[0]}* TODO: todo 4 */ + `; + const todo1 = { text: 'todo 1', tag: 'TODO', lineNumber: 2 }; + const todo2 = { text: 'todo 2', tag: 'TODO', lineNumber: 3 }; + const todo3 = { text: 'todo 3', tag: 'TODO', lineNumber: 4 }; + const todo4 = { text: 'todo 4', tag: 'TODO', lineNumber: 5 }; + + expect(parser({ path: 'f.py', content })).toEqual([todo2]); + expect(parser({ path: 'f.lua', content })).toEqual([todo1]); + expect(parser({ path: 'f.java', content })).toEqual([todo3, todo4]); + expect(parser({ path: 'f.sh', content })).toEqual([todo2]); + expect(parser({ path: 'f.sql', content })).toEqual([todo1, todo3, todo4]); + expect(parser({ path: 'f.json', content })).toEqual([]); + }); +}); diff --git a/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts b/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts index 1abbb901db..79d54e2769 100644 --- a/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts +++ b/plugins/todo-backend/src/lib/TodoReader/createTodoParser.ts @@ -19,23 +19,22 @@ import { parse } from 'leasot'; import { TodoParser } from './types'; export type TodoParserOptions = { - tags?: string[]; + /** Custom tags to support in addition to TODO and FIXME */ + additionalTags?: string[]; }; export function createTodoParser(options: TodoParserOptions = {}): TodoParser { - const { tags = ['TODO', 'FIXME'] } = options; - return ({ content, path }) => { try { const comments = parse(content, { - customTags: tags, + customTags: options.additionalTags, extension: extname(path), }); return comments.map(comment => ({ text: comment.text, tag: comment.tag, - author: comment.ref, + author: comment.ref || undefined, lineNumber: comment.line, })); } catch /* ignore unsupported extensions */ { From 9393f7dfe7a02408443c989357a594d8d311a6f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Mar 2021 11:50:13 +0100 Subject: [PATCH 36/42] todo-backend: add tests for TodoScmReader and make constructor public Signed-off-by: Patrik Oldsberg --- .../src/lib/TodoReader/TodoScmReader.test.ts | 186 ++++++++++++++++++ .../src/lib/TodoReader/TodoScmReader.ts | 12 +- 2 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts new file mode 100644 index 0000000000..83cc8b5ecf --- /dev/null +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts @@ -0,0 +1,186 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { + getVoidLogger, + ReadTreeResponse, + UrlReader, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { NotFoundError, NotModifiedError } from '@backstage/errors'; +import { ScmIntegrations } from '@backstage/integration'; +import { createTodoParser } from './createTodoParser'; +import { TodoScmReader } from './TodoScmReader'; + +function mockReader(): jest.Mocked { + return { + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + } as jest.Mocked; +} + +describe('TodoScmReader', () => { + it('should be created from config', () => { + const todoReader = TodoScmReader.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), + reader: mockReader(), + }); + expect(todoReader).toEqual(expect.any(TodoScmReader)); + }); + + it('should read TODOs', async () => { + const reader = mockReader(); + const todoReader = new TodoScmReader({ + logger: getVoidLogger(), + reader, + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + }); + + reader.readTree.mockResolvedValueOnce({ + etag: 'my-etag', + files: async () => [ + { + content: async () => Buffer.from('// TODO: my-todo', 'utf8'), + path: 'my-folder/my-file.js', + }, + ], + } as ReadTreeResponse); + + const url = 'https://github.com/backstage/backstage/catalog-info.yaml'; + const expected = { + items: [ + { + text: 'my-todo', + tag: 'TODO', + lineNumber: 1, + repoFilePath: 'my-folder/my-file.js', + viewUrl: + 'https://github.com/backstage/backstage/my-folder/my-file.js#L1', + }, + ], + }; + + await expect(todoReader.readTodos({ url })).resolves.toEqual(expected); + + expect(reader.readTree).toHaveBeenCalledTimes(1); + expect(reader.readTree).toHaveBeenCalledWith( + 'https://github.com/backstage/backstage/catalog-info.yaml', + { + etag: undefined, + filter: expect.any(Function), + }, + ); + + // Filter function should filter out dotfiles + const filterFunc = reader.readTree.mock.calls[0][1]!.filter!; + expect(filterFunc('my-file.js')).toBe(true); + expect(filterFunc('my-folder/my-file.js')).toBe(true); + expect(filterFunc('.my-file.js')).toBe(false); + expect(filterFunc('.my-folder/my-file.js')).toBe(false); + expect(filterFunc('my-folder/.my-file.js')).toBe(false); + + // A NotModifiedError should return a cached result + reader.readTree.mockRejectedValueOnce(new NotModifiedError('nope')); + await expect(todoReader.readTodos({ url })).resolves.toEqual(expected); + + expect(reader.readTree).toHaveBeenCalledTimes(2); + expect(reader.readTree).toHaveBeenLastCalledWith( + 'https://github.com/backstage/backstage/catalog-info.yaml', + { + etag: 'my-etag', + filter: expect.any(Function), + }, + ); + + // Other errors should re-throw + reader.readTree.mockRejectedValueOnce(new NotFoundError('not found')); + await expect(todoReader.readTodos({ url })).rejects.toThrow('not found'); + }); + + it('should use custom parser', async () => { + const parser = jest.fn(createTodoParser({ additionalTags: ['XXX'] })); + const reader = mockReader(); + const todoReader = new TodoScmReader({ + logger: getVoidLogger(), + reader, + parser, + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + }); + + reader.readTree.mockResolvedValueOnce({ + files: async () => [ + { + content: async () => Buffer.from('-- XXX: my-todo', 'utf8'), + path: 'my-file.lua', + }, + ], + } as ReadTreeResponse); + + await expect( + todoReader.readTodos({ + url: 'https://github.com/backstage/backstage/catalog-info.yaml', + }), + ).resolves.toEqual({ + items: [ + { + text: 'my-todo', + tag: 'XXX', + lineNumber: 1, + repoFilePath: 'my-file.lua', + viewUrl: 'https://github.com/backstage/backstage/my-file.lua#L1', + }, + ], + }); + expect(parser).toHaveBeenCalledTimes(1); + expect(parser).toHaveBeenCalledWith({ + content: '-- XXX: my-todo', + path: 'my-file.lua', + }); + }); + + it('should log and ignore parser errors', async () => { + const logger = getVoidLogger(); + const errorSpy = jest.spyOn(logger, 'error'); + const reader = mockReader(); + const todoReader = new TodoScmReader({ + logger, + reader, + parser() { + throw new Error('failed to parse'); + }, + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + }); + + reader.readTree.mockResolvedValueOnce({ + files: async () => [ + { + content: async () => Buffer.from('# XXX: my-todo', 'utf8'), + path: 'my-file.sh', + }, + ], + } as ReadTreeResponse); + + await expect( + todoReader.readTodos({ + url: 'https://github.com/o/r/catalog-info.yaml', + }), + ).resolves.toEqual({ items: [] }); + expect(errorSpy).toHaveBeenCalledWith( + 'Failed to parse TODO in https://github.com/o/r/catalog-info.yaml at my-file.sh, Error: failed to parse', + ); + }); +}); diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 07ebdf97eb..f13b0c05df 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -31,6 +31,7 @@ import { createTodoParser } from './createTodoParser'; type Options = { logger: Logger; reader: UrlReader; + integrations: ScmIntegrations; parser?: TodoParser; }; @@ -47,15 +48,18 @@ export class TodoScmReader implements TodoReader { private readonly cache = new Map(); - static fromConfig(config: Config, options: Options) { - return new TodoScmReader(options, ScmIntegrations.fromConfig(config)); + static fromConfig(config: Config, options: Omit) { + return new TodoScmReader({ + ...options, + integrations: ScmIntegrations.fromConfig(config), + }); } - private constructor(options: Options, integrations: ScmIntegrations) { + constructor(options: Options) { this.logger = options.logger; this.reader = options.reader; this.parser = options.parser ?? createTodoParser(); - this.integrations = integrations; + this.integrations = options.integrations; } async readTodos({ url }: ReadTodosOptions): Promise { From b2f1ac111b99eb3afdda3c62e4f7e5b7ca42da03 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Mar 2021 11:57:44 +0100 Subject: [PATCH 37/42] todo-backend: add index test Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/src/index.test.ts | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 plugins/todo-backend/src/index.test.ts diff --git a/plugins/todo-backend/src/index.test.ts b/plugins/todo-backend/src/index.test.ts new file mode 100644 index 0000000000..5b43267573 --- /dev/null +++ b/plugins/todo-backend/src/index.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * as index from './index'; + +describe('imports', () => { + it('should export known symbols only', () => { + expect(index).toEqual({ + createRouter: expect.any(Function), + createTodoParser: expect.any(Function), + TodoScmReader: expect.any(Function), + TodoReaderService: expect.any(Function), + }); + }); +}); From 96d862bd1e92d38d532f246a1d7970670d8b9488 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Mar 2021 13:29:55 +0100 Subject: [PATCH 38/42] todo-backend: add TodoReaderService tests + tweaks Signed-off-by: Patrik Oldsberg --- .../src/service/TodoReaderService.test.ts | 362 ++++++++++++++++++ .../src/service/TodoReaderService.ts | 14 +- 2 files changed, 369 insertions(+), 7 deletions(-) create mode 100644 plugins/todo-backend/src/service/TodoReaderService.test.ts diff --git a/plugins/todo-backend/src/service/TodoReaderService.test.ts b/plugins/todo-backend/src/service/TodoReaderService.test.ts new file mode 100644 index 0000000000..dacc4118d9 --- /dev/null +++ b/plugins/todo-backend/src/service/TodoReaderService.test.ts @@ -0,0 +1,362 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { CatalogApi } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { TodoItem, TodoReader } from '../lib'; +import { TodoReaderService } from './TodoReaderService'; + +const entityName = { + kind: 'Component', + namespace: 'default', + name: 'my-component', +}; + +const mockEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }, + }, + spec: { + type: 'library', + }, +}; + +function mockCatalogClient(entity?: Entity): jest.Mocked { + const mock = { + addLocation: jest.fn(), + getEntities: jest.fn(), + getEntityByName: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + }; + if (entity) { + mock.getEntityByName.mockReturnValue(entity); + } + return mock; +} + +function mockTodoReader(items?: TodoItem[]): jest.Mocked { + const mock = { + readTodos: jest.fn(), + }; + if (items) { + mock.readTodos.mockReturnValue({ items }); + } + return mock; +} + +function makeTodo(text: string, extra?: Partial): TodoItem { + return { text, tag: 'TODO', ...extra }; +} + +describe('TodoReaderService', () => { + it('should be created', () => { + const service = new TodoReaderService({ + todoReader: mockTodoReader(), + catalogClient: mockCatalogClient(), + }); + expect(service.listTodos).toEqual(expect.any(Function)); + }); + + it('should list 0 todos', async () => { + const todoReader = mockTodoReader([]); + const catalogClient = mockCatalogClient(mockEntity); + const service = new TodoReaderService({ todoReader, catalogClient }); + + await expect(service.listTodos({ entity: entityName })).resolves.toEqual({ + items: [], + totalCount: 0, + offset: 0, + limit: 10, + }); + expect(catalogClient.getEntityByName).toHaveBeenCalledWith(entityName); + }); + + it('should list, order, and filter todos', async () => { + const todo1 = makeTodo('todo1'); + const todo2 = makeTodo('todo2', { author: 'foo' }); + const todo3 = makeTodo('todo3', { + author: 'bar', + repoFilePath: 'a/b/c.js', + }); + const todo4 = makeTodo('todo4', { author: 'baz', viewUrl: 'example.com' }); + const todo5 = makeTodo('todo5', { tag: 'FIXME' }); + + const todoReader = mockTodoReader([todo1, todo2, todo3, todo4, todo5]); + const catalogClient = mockCatalogClient({ + ...mockEntity, + metadata: { + ...mockEntity.metadata, + annotations: { + ['backstage.io/source-location']: + 'url:https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }, + }, + }); + const service = new TodoReaderService({ todoReader, catalogClient }); + + await expect(service.listTodos({ entity: entityName })).resolves.toEqual({ + items: [todo1, todo2, todo3, todo4, todo5], + totalCount: 5, + offset: 0, + limit: 10, + }); + + await expect( + service.listTodos({ entity: entityName, limit: 3 }), + ).resolves.toEqual({ + items: [todo1, todo2, todo3], + totalCount: 5, + offset: 0, + limit: 3, + }); + + await expect( + service.listTodos({ entity: entityName, offset: 2 }), + ).resolves.toEqual({ + items: [todo3, todo4, todo5], + totalCount: 5, + offset: 2, + limit: 10, + }); + + await expect( + service.listTodos({ + entity: entityName, + orderBy: { field: 'author', direction: 'asc' }, + }), + ).resolves.toEqual({ + items: [todo3, todo4, todo2, todo1, todo5], + totalCount: 5, + offset: 0, + limit: 10, + }); + + await expect( + service.listTodos({ + entity: entityName, + orderBy: { field: 'author', direction: 'desc' }, + offset: -2, + }), + ).resolves.toEqual({ + items: [todo2, todo4, todo3, todo1, todo5], + totalCount: 5, + offset: 0, + limit: 10, + }); + + await expect( + service.listTodos({ + entity: entityName, + orderBy: { field: 'text', direction: 'desc' }, + offset: 2, + limit: 2, + }), + ).resolves.toEqual({ + items: [todo3, todo2], + totalCount: 5, + offset: 2, + limit: 2, + }); + + await expect( + service.listTodos({ + entity: entityName, + filters: [{ field: 'text', value: '*4' }], + }), + ).resolves.toEqual({ items: [todo4], totalCount: 1, offset: 0, limit: 10 }); + + await expect( + service.listTodos({ + entity: entityName, + filters: [{ field: 'author', value: 'ba*' }], + }), + ).resolves.toEqual({ + items: [todo3, todo4], + totalCount: 2, + offset: 0, + limit: 10, + }); + + await expect( + service.listTodos({ + entity: entityName, + filters: [{ field: 'author', value: 'ba*' }], + limit: 1, + }), + ).resolves.toEqual({ items: [todo3], totalCount: 2, offset: 0, limit: 1 }); + + await expect( + service.listTodos({ + entity: entityName, + filters: [ + { field: 'author', value: 'ba*' }, + { field: 'tag', value: 'FIXME' }, + ], + }), + ).resolves.toEqual({ items: [], totalCount: 0, offset: 0, limit: 10 }); + + await expect( + service.listTodos({ + entity: entityName, + filters: [{ field: 'tag', value: 'FIXME' }], + limit: 500, + }), + ).resolves.toEqual({ items: [todo5], totalCount: 1, offset: 0, limit: 50 }); + + await expect( + service.listTodos({ entity: entityName, limit: -5 }), + ).resolves.toEqual({ items: [], totalCount: 5, offset: 0, limit: 0 }); + }); + + it('should have configurable pagination', async () => { + const todo1 = makeTodo('todo1'); + const todo2 = makeTodo('todo2'); + const todo3 = makeTodo('todo3'); + const todo4 = makeTodo('todo4'); + const todo5 = makeTodo('todo5'); + + const todoReader = mockTodoReader([todo1, todo2, todo3, todo4, todo5]); + const catalogClient = mockCatalogClient(mockEntity); + const service = new TodoReaderService({ + todoReader, + catalogClient, + defaultPageSize: 2, + maxPageSize: 3, + }); + + await expect(service.listTodos({ entity: entityName })).resolves.toEqual({ + items: [todo1, todo2], + totalCount: 5, + offset: 0, + limit: 2, + }); + await expect( + service.listTodos({ entity: entityName, limit: 1 }), + ).resolves.toEqual({ + items: [todo1], + totalCount: 5, + offset: 0, + limit: 1, + }); + await expect( + service.listTodos({ entity: entityName, limit: 3 }), + ).resolves.toEqual({ + items: [todo1, todo2, todo3], + totalCount: 5, + offset: 0, + limit: 3, + }); + await expect( + service.listTodos({ entity: entityName, limit: 4 }), + ).resolves.toEqual({ + items: [todo1, todo2, todo3], + totalCount: 5, + offset: 0, + limit: 3, + }); + }); + + it('should require an entity', async () => { + const service = new TodoReaderService({ + todoReader: mockTodoReader(), + catalogClient: mockCatalogClient(), + }); + await expect(service.listTodos({})).rejects.toThrow( + 'Entity filter is required to list TODOs', + ); + }); + + it('should throw if entity is not found', async () => { + const todoReader = mockTodoReader([]); + const catalogClient = mockCatalogClient(); + const service = new TodoReaderService({ todoReader, catalogClient }); + + await expect(service.listTodos({ entity: entityName })).rejects.toEqual( + expect.objectContaining({ + name: 'NotFoundError', + message: 'Entity not found, Component:default/my-component', + }), + ); + expect(catalogClient.getEntityByName).toHaveBeenCalledWith(entityName); + }); + + it('should throw if entity does not have a location', async () => { + const todoReader = mockTodoReader([]); + const catalogClient = mockCatalogClient({ + ...mockEntity, + metadata: { ...mockEntity.metadata, annotations: undefined }, + }); + const service = new TodoReaderService({ todoReader, catalogClient }); + + await expect(service.listTodos({ entity: entityName })).rejects.toEqual( + expect.objectContaining({ + name: 'InputError', + message: + 'No entity location annotation found for Component:my-component', + }), + ); + }); + + it('should throw if entity has an invalid location', async () => { + const todoReader = mockTodoReader([]); + const catalogClient = mockCatalogClient({ + ...mockEntity, + metadata: { + ...mockEntity.metadata, + annotations: { + ['backstage.io/managed-by-location']: 'file:../info.yaml', + }, + }, + }); + const service = new TodoReaderService({ todoReader, catalogClient }); + + await expect(service.listTodos({ entity: entityName })).rejects.toEqual( + expect.objectContaining({ + name: 'InputError', + message: `Invalid entity location type for Component:my-component, got 'file'`, + }), + ); + }); + + it('should throw if entity has an invalid source location', async () => { + const todoReader = mockTodoReader([]); + const catalogClient = mockCatalogClient({ + ...mockEntity, + metadata: { + ...mockEntity.metadata, + annotations: { + ['backstage.io/source-location']: 'file:../info.yaml', + }, + }, + }); + const service = new TodoReaderService({ todoReader, catalogClient }); + + await expect(service.listTodos({ entity: entityName })).rejects.toEqual( + expect.objectContaining({ + name: 'InputError', + message: `Invalid entity source location type for Component:my-component, got 'file'`, + }), + ); + }); +}); diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 994866db93..2f20aab0f1 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -15,7 +15,7 @@ */ import { InputError, NotFoundError } from '@backstage/errors'; -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi } from '@backstage/catalog-client'; import { LOCATION_ANNOTATION, SOURCE_LOCATION_ANNOTATION, @@ -31,7 +31,7 @@ const DEFAULT_MAX_PAGE_SIZE = 50; type Options = { todoReader: TodoReader; - catalogClient: CatalogClient; + catalogClient: CatalogApi; maxPageSize?: number; defaultPageSize?: number; }; @@ -45,7 +45,7 @@ function wildcardRegex(str: string): RegExp { export class TodoReaderService implements TodoService { private readonly todoReader: TodoReader; - private readonly catalogClient: CatalogClient; + private readonly catalogClient: CatalogApi; private readonly maxPageSize: number; private readonly defaultPageSize: number; @@ -100,7 +100,7 @@ export class TodoReaderService implements TodoService { const field2 = item2[field]; if (field1 && field2) { - return dir * field1?.localeCompare(field2, 'en-US'); + return dir * field1.localeCompare(field2, 'en-US'); } else if (field1 && !field2) { return -1; } else if (!field1 && field2) { @@ -127,7 +127,7 @@ export class TodoReaderService implements TodoService { throw new InputError( `Invalid entity source location type for ${serializeEntityRef( entity, - )}, got ${parsed.type}`, + )}, got '${parsed.type}'`, ); } return parsed.target; @@ -138,9 +138,9 @@ export class TodoReaderService implements TodoService { const parsed = parseLocationReference(location); if (parsed.type !== 'url') { throw new InputError( - `Invalid entity source location type for ${serializeEntityRef( + `Invalid entity location type for ${serializeEntityRef( entity, - )}, got ${parsed.type}`, + )}, got '${parsed.type}'`, ); } return parsed.target; From f5cfded56a06c1a830a0e7290033c4c1941a9b35 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Mar 2021 15:56:18 +0100 Subject: [PATCH 39/42] todo-backend: add router tests + tweaks Signed-off-by: Patrik Oldsberg --- .../todo-backend/src/service/router.test.ts | 131 +++++++++++++++++- plugins/todo-backend/src/service/router.ts | 13 +- 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/plugins/todo-backend/src/service/router.test.ts b/plugins/todo-backend/src/service/router.test.ts index 8fda603937..13574d9381 100644 --- a/plugins/todo-backend/src/service/router.test.ts +++ b/plugins/todo-backend/src/service/router.test.ts @@ -16,8 +16,14 @@ import express from 'express'; import request from 'supertest'; +import { errorHandler } from '@backstage/backend-common'; -import { createRouter } from './router'; +import { + createRouter, + parseFilterParam, + parseIntegerParam, + parseOrderByParam, +} from './router'; import { TodoService } from './types'; const mockListBody = { @@ -27,6 +33,18 @@ const mockListBody = { limit: 10, }; +function matchErrorResponse(status: number, name: string, message: string) { + return { + status, + body: expect.objectContaining({ + error: { name, message }, + response: { + statusCode: status, + }, + }), + }; +} + describe('createRouter', () => { let app: express.Express; const mockService: jest.Mocked = { @@ -37,7 +55,7 @@ describe('createRouter', () => { const router = await createRouter({ todoService: mockService, }); - app = express().use(router); + app = express().use(router).use(errorHandler()); }); beforeEach(() => { @@ -89,5 +107,114 @@ describe('createRouter', () => { limit: undefined, }); }); + + it('rejects invalid queries', async () => { + await expect( + request(app).get('/v1/todos?entity=k:n&entity=k:n'), + ).resolves.toMatchObject( + matchErrorResponse(400, 'InputError', 'entity query must be a string'), + ); + + await expect( + request(app).get('/v1/todos?entity=:n'), + ).resolves.toMatchObject( + matchErrorResponse( + 400, + 'InputError', + 'Invalid entity ref, TypeError: Entity reference ":n" was not on the form [:][/]', + ), + ); + + await expect( + request(app).get('/v1/todos?offset=1.5'), + ).resolves.toMatchObject( + matchErrorResponse( + 400, + 'InputError', + 'invalid offset query, not an integer', + ), + ); + + expect(mockService.listTodos).not.toHaveBeenCalled(); + }); + }); +}); + +describe('parseIntegerParam', () => { + it('should parse a param', () => { + expect(parseIntegerParam('1', 'ctx')).toBe(1); + }); + + it('should reject invalid params', () => { + expect(() => parseIntegerParam(['1'], 'ctx')).toThrow( + 'invalid ctx, must be a string', + ); + expect(() => parseIntegerParam('1.5', 'ctx')).toThrow( + 'invalid ctx, not an integer', + ); + expect(() => parseIntegerParam('foo', 'ctx')).toThrow( + 'invalid ctx, not an integer', + ); + expect(() => parseIntegerParam('1foo', 'ctx')).toThrow( + 'invalid ctx, not an integer', + ); + }); +}); + +describe('parseOrderByParam', () => { + it('should parse a param', () => { + expect(parseOrderByParam('a=asc', ['a'])).toEqual({ + field: 'a', + direction: 'asc', + }); + expect(parseOrderByParam('a=desc', ['a'])).toEqual({ + field: 'a', + direction: 'desc', + }); + }); + + it('should reject invalid params', () => { + expect(() => parseOrderByParam(['a=asc'], ['a'])).toThrow( + 'invalid orderBy query, must be a string', + ); + expect(() => parseOrderByParam('a=desc', ['b', 'c'])).toThrow( + 'invalid orderBy field, must be one of b, c', + ); + expect(() => parseOrderByParam('b=down', ['a'])).toThrow( + `invalid orderBy query, order direction must be 'asc' or 'desc'`, + ); + expect(() => parseOrderByParam('=asc', ['a'])).toThrow( + 'invalid orderBy query, field name is empty', + ); + }); +}); + +describe('parseFilterParam', () => { + it('should parse a param', () => { + expect(parseFilterParam('a=b', ['a'])).toEqual([ + { field: 'a', value: 'b' }, + ]); + expect(parseFilterParam(['a=b=c', 'c=d*'], ['a', 'c'])).toEqual([ + { field: 'a', value: 'b=c' }, + { field: 'c', value: 'd*' }, + ]); + }); + + it('should reject invalid params', () => { + expect(() => parseFilterParam({ a: 'a=b' }, ['a'])).toThrow( + 'invalid filter query, must be a string or list of strings', + ); + expect(() => parseFilterParam('a=b', ['b', 'c'])).toThrow( + 'invalid filter field, must be one of b, c', + ); + expect(() => parseFilterParam('b', ['a'])).toThrow( + `invalid filter query, must separate field from value using '='`, + ); + expect(() => parseFilterParam('=a', ['a'])).toThrow( + `invalid filter query, must separate field from value using '='`, + ); + expect(() => parseFilterParam('a=', ['a'])).toThrow( + 'invalid filter query, value may not be empty', + ); }); }); diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index d2eb7278f8..9c06e19a20 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -72,7 +72,10 @@ export async function createRouter( return router; } -function parseIntegerParam(str: unknown, ctx: string): number | undefined { +export function parseIntegerParam( + str: unknown, + ctx: string, +): number | undefined { if (str === undefined) { return undefined; } @@ -80,13 +83,13 @@ function parseIntegerParam(str: unknown, ctx: string): number | undefined { throw new InputError(`invalid ${ctx}, must be a string`); } const parsed = parseInt(str, 10); - if (!Number.isInteger(parsed)) { + if (!Number.isInteger(parsed) || String(parsed) !== str) { throw new InputError(`invalid ${ctx}, not an integer`); } return parsed; } -function parseOrderByParam( +export function parseOrderByParam( str: unknown, allowedFields: T, ): { field: T[number]; direction: 'asc' | 'desc' } | undefined { @@ -108,13 +111,13 @@ function parseOrderByParam( if (field && !allowedFields.includes(field)) { throw new InputError( - `invalid orderBy query, must be one of ${allowedFields.join(', ')}`, + `invalid orderBy field, must be one of ${allowedFields.join(', ')}`, ); } return { field, direction }; } -function parseFilterParam( +export function parseFilterParam( str: unknown, allowedFields: T, ): { field: T[number]; value: string }[] | undefined { From 19be64f5ea91f0f757bf376cb4db7e5b0889a688 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Mar 2021 16:00:19 +0100 Subject: [PATCH 40/42] todo: added index test Signed-off-by: Patrik Oldsberg --- plugins/todo/src/index.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 plugins/todo/src/index.test.ts diff --git a/plugins/todo/src/index.test.ts b/plugins/todo/src/index.test.ts new file mode 100644 index 0000000000..f99455c18b --- /dev/null +++ b/plugins/todo/src/index.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * as index from './index'; + +describe('imports', () => { + it('should export known symbols only', () => { + expect(index).toEqual({ + todoPlugin: expect.any(Object), + todoApiRef: expect.any(Object), + EntityTodoContent: expect.any(Function), + }); + }); +}); From b0202804863cb17c786d759a884653e2f4d10d37 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Mar 2021 20:08:59 +0100 Subject: [PATCH 41/42] microsite: add todo plugin to marketplace Signed-off-by: Patrik Oldsberg --- microsite/data/plugins/todo.yaml | 9 +++++++++ microsite/static/img/todo-logo.png | Bin 0 -> 16908 bytes 2 files changed, 9 insertions(+) create mode 100644 microsite/data/plugins/todo.yaml create mode 100644 microsite/static/img/todo-logo.png diff --git a/microsite/data/plugins/todo.yaml b/microsite/data/plugins/todo.yaml new file mode 100644 index 0000000000..44128c40b2 --- /dev/null +++ b/microsite/data/plugins/todo.yaml @@ -0,0 +1,9 @@ +--- +title: TODO +author: Spotify +authorUrl: https://github.com/spotify +category: Discovery +description: Browse TODO comments in your project's source code. +documentation: https://github.com/backstage/backstage/tree/master/plugins/todo +iconUrl: https://backstage.io/img/todo-logo.png +npmPackageName: '@backstage/plugin-todo' diff --git a/microsite/static/img/todo-logo.png b/microsite/static/img/todo-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..eae540147955c2c92c222ff31daa9afe5b28658b GIT binary patch literal 16908 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 b;_yLHG?d@T%GToXU&mx56vWF#4L|=cU+Dx9 literal 0 HcmV?d00001 From 79f87efc1bf975c27af45506c1bfe63bc8f10bb9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Mar 2021 22:11:19 +0100 Subject: [PATCH 42/42] todo-backend: clarify how the location of the source code is determined Signed-off-by: Patrik Oldsberg --- plugins/todo-backend/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/todo-backend/README.md b/plugins/todo-backend/README.md index e5f465466d..61ca5202d0 100644 --- a/plugins/todo-backend/README.md +++ b/plugins/todo-backend/README.md @@ -36,6 +36,11 @@ export default async function createPlugin({ } ``` +## Scanned Files + +The included `TodoReaderService` and `TodoScmReader` works by reading source code of to the entity that is being viewed. The location source code is determined by the value of the [`backstage.io/source-location` +](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location) annotation of the entity, and if that is missing it falls back to the [`backstage.io/managed-by-location `](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiomanaged-by-location) annotation. Only `url` locations are currently supported, meaning locally configured `file` locations won't work. Also note that dot-files and folders are ignored. + ## Parser Configuration The `TodoScmReader` accepts a `TodoParser` option, which can be used to configure your own parser. The default one is based on [Leasot](https://github.com/pgilad/leasot) and supports a wide range of languages. You can add to the list of supported tags by configuring your own version of the built-in parser, for example: