From 6f58e92fc76107f31f86fca2d23ab6a02fc41b25 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 29 Apr 2020 16:34:35 +0200 Subject: [PATCH 01/22] Create plugin draft --- README.html | 138 ++++++++++++++++ packages/app/package.json | 1 + packages/app/src/plugins.ts | 2 +- plugins/circleci/.eslintrc.js | 3 + plugins/circleci/README.md | 6 + plugins/circleci/package.json | 38 +++++ .../ExampleComponent.test.tsx | 34 ++++ .../ExampleComponent/ExampleComponent.tsx | 59 +++++++ .../src/components/ExampleComponent/index.ts | 17 ++ .../ExampleFetchComponent.test.tsx | 28 ++++ .../ExampleFetchComponent.tsx | 151 ++++++++++++++++++ .../components/ExampleFetchComponent/index.ts | 17 ++ plugins/circleci/src/index.ts | 17 ++ plugins/circleci/src/plugin.test.ts | 23 +++ plugins/circleci/src/plugin.ts | 24 +++ plugins/circleci/src/setupTests.ts | 18 +++ plugins/circleci/tsconfig.json | 7 + yarn.lock | 23 ++- 18 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 README.html create mode 100644 plugins/circleci/.eslintrc.js create mode 100644 plugins/circleci/README.md create mode 100644 plugins/circleci/package.json create mode 100644 plugins/circleci/src/components/ExampleComponent/ExampleComponent.test.tsx create mode 100644 plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx create mode 100644 plugins/circleci/src/components/ExampleComponent/index.ts create mode 100644 plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx create mode 100644 plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx create mode 100644 plugins/circleci/src/components/ExampleFetchComponent/index.ts create mode 100644 plugins/circleci/src/index.ts create mode 100644 plugins/circleci/src/plugin.test.ts create mode 100644 plugins/circleci/src/plugin.ts create mode 100644 plugins/circleci/src/setupTests.ts create mode 100644 plugins/circleci/tsconfig.json diff --git a/README.html b/README.html new file mode 100644 index 0000000000..e9a0e9c19e --- /dev/null +++ b/README.html @@ -0,0 +1,138 @@ + + + + + + +README.html + + + + + +
+headline +
headline
+
+ +

Backstage

+ +

License + +Discord +Code style +

+ +

What is Backstage?

+ +

Backstage is an open platform for building developer portals.

+ +

The philosophy behind Backstage is simple: Don’t expose your engineers to the full complexity of your infrastructure tooling. Engineers should be shipping code β€” not figuring out a whole new toolset every time they want to implement the basics. Backstage allows you add “stuff” (tooling, services, features, etc.) by adding a plugin, instead of building a new tool. This saves you work and avoids the need of your team to learn how to use and support yet another tool.

+ +

For more information go to backstage.io or join our Discord chatroom.

+ +

What problem does Backstage solve?

+ +

As companies grow, their infrastructure systems get messier. Backstage unifies all your infrastructure tooling, services, and documentation with a single, consistent UI.

+ +

This blog post provides more examples of how Backstage is used inside Spotify:

+ +

https://labs.spotify.com/2020/03/17/what-the-heck-is-backstage-anyway/

+ +

Project roadmap

+ +

We created Backstage about 4 years ago. While our internal version of Backstage has had the benefit of time to mature and evolve, the first iteration of our open source version is still nascent. We are envisioning three phases of the project and we have already begun work on various aspects of these phases:

+ + + +

Check out our Milestones and open RFCs how they relate to the three Phases outlined above.

+ +

Our vision for Backstage is for it to become the trusted standard toolbox (read: UX layer) for the open source infrastructure landscape. Think of it like Kubernetes for developer experience. We realize this is an ambitious goal. We can’t do it alone. If this sounds interesting or you’d like to help us shape our product vision, we’d love to talk. You can email me directly: alund@spotify.com.

+ +

Overview

+ +

The Backstage platform consists of a number of different components:

+ + + +

* not yet released

+ +
+overview +
overview
+
+ +

Getting started

+ +

To run a Backstage app, you will need to have the following installed:

+ + + +

After cloning this repo, open a terminal window and start the web app using the following commands from the project root:

+ +
yarn install
+yarn start
+
+ +

The final yarn start command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.

+ +

And thats it! You are good to go πŸ‘

+ +

Next step

+ +

Take a look at the Getting Started guide to learn more about how to extend the functionality with Plugins.

+ +

Documentation

+ + + +

Contributing

+ +

We would love your help in building Backstage! See CONTRIBUTING for more information.

+ +

Community

+ + + +

Or, if you are an open source developer and are interested in joining our team, please reach out to foss-opportunities@spotify.com

+ +

License

+ +

Copyright 2020 Spotify AB.

+ +

Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0

+ + + diff --git a/packages/app/package.json b/packages/app/package.json index 7ccb6c9a91..da1d02a5be 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -20,6 +20,7 @@ "dependencies": { "@backstage/cli": "^0.1.1-alpha.4", "@backstage/core": "^0.1.1-alpha.4", + "@backstage/plugin-circleci": "^0.1.1-alpha.4", "@backstage/plugin-home-page": "^0.1.1-alpha.4", "@backstage/plugin-inventory": "^0.1.1-alpha.4", "@backstage/plugin-lighthouse": "^0.1.1-alpha.4", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index a7e425b869..31546c6ba6 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - export { plugin as HomePagePlugin } from '@backstage/plugin-home-page'; export { plugin as WelcomePlugin } from '@backstage/plugin-welcome'; export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse'; export { plugin as InventoryPlugin } from '@backstage/plugin-inventory'; export { plugin as TechRadar } from '@backstage/plugin-tech-radar'; +export { plugin as Circleci } from '@backstage/plugin-circleci'; diff --git a/plugins/circleci/.eslintrc.js b/plugins/circleci/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/circleci/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/circleci/README.md b/plugins/circleci/README.md new file mode 100644 index 0000000000..46d48e9411 --- /dev/null +++ b/plugins/circleci/README.md @@ -0,0 +1,6 @@ +# Title +Welcome to the circleci plugin! + +## Sub-section 1 + +## Sub-section 2 diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json new file mode 100644 index 0000000000..80603f4029 --- /dev/null +++ b/plugins/circleci/package.json @@ -0,0 +1,38 @@ +{ + "name": "@backstage/plugin-circleci", + "version": "0.1.1-alpha.4", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "clean": "backstage-cli clean" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.4", + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^7.1.2", + "@types/jest": "^24.0.0", + "@types/node": "^12.0.0", + "@types/testing-library__jest-dom": "5.0.2", + "jest-fetch-mock": "^3.0.3" + }, + "dependencies": { + "@backstage/core": "^0.1.1-alpha.4", + "@backstage/theme": "^0.1.1-alpha.4", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "circleci-api": "^4.0.0", + "react": "16.13.1", + "react-dom": "16.13.1", + "react-use": "^13.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/circleci/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/circleci/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 0000000000..ef0f3f9632 --- /dev/null +++ b/plugins/circleci/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,34 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import mockFetch from 'jest-fetch-mock'; +import ExampleComponent from './ExampleComponent'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; + +describe('ExampleComponent', () => { + it('should render', () => { + mockFetch.mockResponse(() => new Promise(() => {})); + const rendered = render( + + + , + ); + expect(rendered.getByText('Welcome to circleci!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 0000000000..72b5e0e798 --- /dev/null +++ b/plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,59 @@ +/* + * 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 React, { FC } from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + pageTheme, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core'; +import ExampleFetchComponent from '../ExampleFetchComponent'; + +const ExampleComponent: FC<{}> = () => ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + + + + + + + + + +
+); + +export default ExampleComponent; diff --git a/plugins/circleci/src/components/ExampleComponent/index.ts b/plugins/circleci/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..e785d45082 --- /dev/null +++ b/plugins/circleci/src/components/ExampleComponent/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 { default } from './ExampleComponent'; diff --git a/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx new file mode 100644 index 0000000000..7fecdc6f11 --- /dev/null +++ b/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import mockFetch from 'jest-fetch-mock'; +import ExampleFetchComponent from './ExampleFetchComponent'; + +describe('ExampleFetchComponent', () => { + it('should render', async () => { + mockFetch.mockResponse(() => new Promise(() => {})); + const rendered = render(); + expect(await rendered.findByTestId('progress')).toBeInTheDocument(); + }); +}); diff --git a/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx new file mode 100644 index 0000000000..7342da7d71 --- /dev/null +++ b/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -0,0 +1,151 @@ +/* + * 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 React, { FC } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Alert from '@material-ui/lab/Alert'; +import { useAsync } from 'react-use'; +import { Progress } from '@backstage/core'; + +import { CircleCI, GitType, CircleCIOptions } from "circleci-api"; + +const CIRCLECI_TOKEN: string = "943aa82531ccaab192b4c4bc614507dff31c094c"; + +// Configure the factory with some defaults +const options: CircleCIOptions = { + // Required for all requests + token: CIRCLECI_TOKEN, // Set your CircleCi API token + + // Optional + // Anything set here can be overriden when making the request + + // Git information is required for project/build/etc endpoints + vcs: { + type: GitType.GITHUB, // default: github + owner: "CircleCITest3", + repo: "circleci-test" + }, + + // Optional query params for requests + // options: { + // branch: "master", // default: master + // } +}; + +const api = new CircleCI(options); + +api.builds() + .then((v) => console.log("token is valid")) + .catch(() => console.error("invalid token")); + + +const useStyles = makeStyles({ + table: { + minWidth: 650, + }, + 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: FC = ({ users }) => { + const classes = useStyles(); + + return ( + + + + + Avatar + Name + Email + Nationality + + + + {users.map(user => ( + + + {user.name.first} + + + {user.name.first} {user.name.last} + + {user.email} + {user.nat} + + ))} + +
+
+ ); +}; + +const ExampleFetchComponent: FC<{}> = () => { + // 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 ; +}; + +export default ExampleFetchComponent; diff --git a/plugins/circleci/src/components/ExampleFetchComponent/index.ts b/plugins/circleci/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 0000000000..28482f9fe1 --- /dev/null +++ b/plugins/circleci/src/components/ExampleFetchComponent/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 { default } from './ExampleFetchComponent'; diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts new file mode 100644 index 0000000000..3a0a0fe2d3 --- /dev/null +++ b/plugins/circleci/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 { plugin } from './plugin'; diff --git a/plugins/circleci/src/plugin.test.ts b/plugins/circleci/src/plugin.test.ts new file mode 100644 index 0000000000..821a503257 --- /dev/null +++ b/plugins/circleci/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { plugin } from './plugin'; + +describe('circleci', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts new file mode 100644 index 0000000000..2c56b4eec2 --- /dev/null +++ b/plugins/circleci/src/plugin.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 { createPlugin } from '@backstage/core'; +import ExampleComponent from './components/ExampleFetchComponent'; + +export const plugin = createPlugin({ + id: 'circleci', + register({ router }) { + router.registerRoute('/circleci', ExampleComponent); + }, +}); diff --git a/plugins/circleci/src/setupTests.ts b/plugins/circleci/src/setupTests.ts new file mode 100644 index 0000000000..1a907ab8e6 --- /dev/null +++ b/plugins/circleci/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * 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 '@testing-library/jest-dom/extend-expect'; +require('jest-fetch-mock').enableMocks(); diff --git a/plugins/circleci/tsconfig.json b/plugins/circleci/tsconfig.json new file mode 100644 index 0000000000..7b73db2f0f --- /dev/null +++ b/plugins/circleci/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "baseUrl": "src" + } +} diff --git a/yarn.lock b/yarn.lock index f17fc840c8..cd2016d0d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5290,6 +5290,13 @@ aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== +axios@^0.19.0: + version "0.19.2" + resolved "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + axobject-query@^2.0.2: version "2.1.2" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.2.tgz#2bdffc0371e643e5f03ba99065d5179b9ca79799" @@ -6460,6 +6467,13 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +circleci-api@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/circleci-api/-/circleci-api-4.0.0.tgz#d773fe68f4a59e1968881269883a23b0805b3546" + integrity sha512-D/THFyhOv6THSkYXJhrOLIOmV7fmyDqgs1+pBFMAqDR+ywXszxa2Dqx1Zw+YD3O2zD2y5LQOifCDT96VidRG7Q== + dependencies: + axios "^0.19.0" + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -7769,7 +7783,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.1.0: +debug@3.1.0, debug@=3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -9499,6 +9513,13 @@ focus-lock@^0.6.6: resolved "https://registry.npmjs.org/focus-lock/-/focus-lock-0.6.6.tgz#98119a755a38cfdbeda0280eaa77e307eee850c7" integrity sha512-Dx69IXGCq1qsUExWuG+5wkiMqVM/zGx/reXSJSLogECwp3x6KeNQZ+NAetgxEFpnC41rD8U3+jRCW68+LNzdtw== +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + follow-redirects@^1.0.0: version "1.10.0" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb" From cbda9adb450aae3d0e7e24eff4935eaa57485968 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 29 Apr 2020 17:05:29 +0200 Subject: [PATCH 02/22] feat: token input and showing list of builds --- .../ExampleComponent/ExampleComponent.tsx | 65 +++++++------ .../ExampleFetchComponent.tsx | 94 ++++++++++--------- plugins/circleci/src/plugin.ts | 2 +- 3 files changed, 90 insertions(+), 71 deletions(-) diff --git a/plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx index 72b5e0e798..966debd11f 100644 --- a/plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx +++ b/plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import React, { FC } from 'react'; -import { Typography, Grid } from '@material-ui/core'; +import React, { FC, useState } from 'react'; +import { Typography, Grid, Input } from '@material-ui/core'; import { InfoCard, Header, @@ -28,32 +28,41 @@ import { } from '@backstage/core'; import ExampleFetchComponent from '../ExampleFetchComponent'; -const ExampleComponent: FC<{}> = () => ( - -
- - -
- - - A description of your plugin goes here. - - - - - - All content should be wrapped in a card like this. - - +const ExampleComponent: FC<{}> = () => { + const [token, setToken] = useState(''); + + return ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + Please paste your CircleCI token here + setToken(e.target.value)} + type="password" + value={token} + > + + + + + + + + - - - - - -
-
-
-); + + + ); +}; export default ExampleComponent; diff --git a/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx index 7342da7d71..c9bed14117 100644 --- a/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx +++ b/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -7,14 +7,14 @@ * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software + * Unless required by applicable law or agreed to in wr iting, 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, { FC } from 'react'; +import React, { FC, useRef, useEffect } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; @@ -22,41 +22,22 @@ import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; -import Alert from '@material-ui/lab/Alert'; +// import Alert from '@material-ui/lab/Alert'; import { useAsync } from 'react-use'; -import { Progress } from '@backstage/core'; +// import { Progress } from '@backstage/core'; -import { CircleCI, GitType, CircleCIOptions } from "circleci-api"; +import { CircleCI, GitType, CircleCIOptions, BuildSummary } from 'circleci-api'; -const CIRCLECI_TOKEN: string = "943aa82531ccaab192b4c4bc614507dff31c094c"; +// const CIRCLECI_TOKEN: string = '943aa82531ccaab192b4c4bc614507dff31c094c'; // Configure the factory with some defaults -const options: CircleCIOptions = { - // Required for all requests - token: CIRCLECI_TOKEN, // Set your CircleCi API token - // Optional - // Anything set here can be overriden when making the request - - // Git information is required for project/build/etc endpoints - vcs: { - type: GitType.GITHUB, // default: github - owner: "CircleCITest3", - repo: "circleci-test" - }, - - // Optional query params for requests - // options: { - // branch: "master", // default: master - // } -}; - -const api = new CircleCI(options); - -api.builds() - .then((v) => console.log("token is valid")) - .catch(() => console.error("invalid token")); +// const api = new CircleCI(options); +// api +// .builds() +// .then(d => console.log('token is valid', d)) +// .catch(() => console.error('invalid token')); const useStyles = makeStyles({ table: { @@ -131,21 +112,50 @@ export const DenseTable: FC = ({ users }) => { ); }; +const options: Partial = { + // Required for all requests + // token: CIRCLECI_TOKEN, // Set your CircleCi API token -const ExampleFetchComponent: FC<{}> = () => { - // 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; - // }, []); + // Optional + // Anything set here can be overriden when making the request - if (loading) { - return ; - } else if (error) { - return {error.message}; - } + // Git information is required for project/build/etc endpoints + vcs: { + type: GitType.GITHUB, // default: github + owner: 'CircleCITest3', + repo: 'circleci-test', + }, - return ; + // Optional query params for requests + // options: { + // branch: "master", // default: master + // } +}; + +const BuildList: React.FC<{ builds: BuildSummary[] }> = ({ builds }) => ( +
    + {builds.map(build => ( +
  • + #{build.build_num} ({build.subject}) +
  • + ))} +
+); +const ExampleFetchComponent: FC<{ token: string }> = ({ token }) => { + const api = useRef(null); + useEffect(() => { + if (token !== '') api.current = new CircleCI({ ...options, token }); + }, [token]); + + const { value, loading, error } = useAsync(() => { + if (api.current) return api.current.builds(); + return Promise.reject('Api token not provided'); + }, [token]); + + if (loading) return
loading
; + if (error) return
{JSON.stringify(error, null, 2)}
; + + return ; }; export default ExampleFetchComponent; diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 2c56b4eec2..4aec241bdb 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createPlugin } from '@backstage/core'; -import ExampleComponent from './components/ExampleFetchComponent'; +import ExampleComponent from './components/ExampleComponent'; export const plugin = createPlugin({ id: 'circleci', From 564c0954ec08827baeead75a678cf60ea87eb760 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 30 Apr 2020 11:29:43 +0200 Subject: [PATCH 03/22] feat(circleci-plugin): nice table view --- .../src/components/CITable/CITable.tsx | 87 ++++++++++ .../circleci/src/components/CITable/index.ts | 1 + .../components/CircleCIFetch/CirleCIFetch.tsx | 88 ++++++++++ .../src/components/CircleCIFetch/index.ts | 1 + .../CircleCIPage.tsx} | 10 +- .../src/components/CircleCIPage/index.ts | 1 + .../ExampleComponent.test.tsx | 34 ---- .../src/components/ExampleComponent/index.ts | 17 -- .../ExampleFetchComponent.test.tsx | 28 --- .../ExampleFetchComponent.tsx | 161 ------------------ .../components/ExampleFetchComponent/index.ts | 17 -- plugins/circleci/src/plugin.ts | 4 +- 12 files changed, 185 insertions(+), 264 deletions(-) create mode 100644 plugins/circleci/src/components/CITable/CITable.tsx create mode 100644 plugins/circleci/src/components/CITable/index.ts create mode 100644 plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx create mode 100644 plugins/circleci/src/components/CircleCIFetch/index.ts rename plugins/circleci/src/components/{ExampleComponent/ExampleComponent.tsx => CircleCIPage/CircleCIPage.tsx} (87%) create mode 100644 plugins/circleci/src/components/CircleCIPage/index.ts delete mode 100644 plugins/circleci/src/components/ExampleComponent/ExampleComponent.test.tsx delete mode 100644 plugins/circleci/src/components/ExampleComponent/index.ts delete mode 100644 plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx delete mode 100644 plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx delete mode 100644 plugins/circleci/src/components/ExampleFetchComponent/index.ts diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/components/CITable/CITable.tsx new file mode 100644 index 0000000000..21cb03ce6c --- /dev/null +++ b/plugins/circleci/src/components/CITable/CITable.tsx @@ -0,0 +1,87 @@ +// Idea for this component to be somehow reusable representation of CI table view +import React, { FC } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; + +const useStyles = makeStyles({ + table: { + minWidth: 650, + }, + avatar: { + height: 32, + width: 32, + borderRadius: '50%', + }, +}); + +export type CITableBuildInfo = { + id: string; + buildName: string; + source: { + branchName: string; + commit: { + hash: string; + url: string; + }; + }; + status: 'success' | 'pending' | 'error'; + tests?: { + total: number; + passed: number; + skipped: number; + failed: number; + testUrl: string; //fixme better name + }; + onRetriggerClick: () => void; +}; + +export const CITable: FC<{ + builds: CITableBuildInfo[]; +}> = ({ builds }) => { + const classes = useStyles(); + + return ( + + + + + Build + Source + Status + Tests + Retrigger + + + + {builds.map(build => ( + + {build.buildName} + +
+
{build.source.branchName}
+
{build.source.commit.hash}
+
+
+ {build.status} + + {build.tests && ( + <> + {build.tests.passed}/{build.tests.total} ( + {build.tests.failed ? build.tests.failed + ', ' : ''} + {build.tests.skipped ? build.tests.skipped : ''}) + + )} + + Retrigger +
+ ))} +
+
+
+ ); +}; diff --git a/plugins/circleci/src/components/CITable/index.ts b/plugins/circleci/src/components/CITable/index.ts new file mode 100644 index 0000000000..279634bce6 --- /dev/null +++ b/plugins/circleci/src/components/CITable/index.ts @@ -0,0 +1 @@ +export { CITable, CITableBuildInfo } from './CITable'; diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx new file mode 100644 index 0000000000..9ca7f93a0a --- /dev/null +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -0,0 +1,88 @@ +/* + * 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 wr iting, 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, { FC, useRef, useEffect } from 'react'; + +// import Alert from '@material-ui/lab/Alert'; +import { useAsync } from 'react-use'; +// import { Progress } from '@backstage/core'; + +import { CircleCI, GitType, CircleCIOptions, BuildSummary } from 'circleci-api'; + +import { CITable, CITableBuildInfo } from '../CITable'; +const options: Partial = { + // Required for all requests + // token: CIRCLECI_TOKEN, // Set your CircleCi API token + + // Optional + // Anything set here can be overriden when making the request + + // Git information is required for project/build/etc endpoints + vcs: { + type: GitType.GITHUB, // default: github + owner: 'CircleCITest3', + repo: 'circleci-test', + }, + + // Optional query params for requests + // options: { + // branch: "master", // default: master + // } +}; + +const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { + return buildsData.map(buildData => { + const tableBuildInfo: CITableBuildInfo = { + id: String(buildData.build_num), + buildName: String(buildData.subject), + onRetriggerClick: () => null, + source: { + branchName: String(buildData.branch), + commit: { + hash: String(buildData.vcs_revision), + url: 'todo', + }, + }, + status: String(buildData.status) as 'success' | 'error' | 'pending', + tests: { + failed: 0, + passed: 10, + skipped: 3, + testUrl: 'nourlnow', + total: 13, + }, + }; + return tableBuildInfo; + }); +}; + +export const CircleCIFetch: FC<{ token: string }> = ({ token }) => { + const api = useRef(null); + useEffect(() => { + if (token !== '') api.current = new CircleCI({ ...options, token }); + }, [token]); + + const { value, loading, error } = useAsync(() => { + if (api.current) return api.current.builds(); + return Promise.reject('Api token not provided'); + }, [token]); + + if (loading) return
loading
; + if (error) return
{JSON.stringify(error, null, 2)}
; + + const builds = transform(value || []); + return ; +}; diff --git a/plugins/circleci/src/components/CircleCIFetch/index.ts b/plugins/circleci/src/components/CircleCIFetch/index.ts new file mode 100644 index 0000000000..e204a791cd --- /dev/null +++ b/plugins/circleci/src/components/CircleCIFetch/index.ts @@ -0,0 +1 @@ +export { CircleCIFetch } from './CirleCIFetch'; diff --git a/plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx similarity index 87% rename from plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx rename to plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx index 966debd11f..f27ac80a6f 100644 --- a/plugins/circleci/src/components/ExampleComponent/ExampleComponent.tsx +++ b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx @@ -26,9 +26,9 @@ import { HeaderLabel, SupportButton, } from '@backstage/core'; -import ExampleFetchComponent from '../ExampleFetchComponent'; +import { CircleCIFetch } from '../CircleCIFetch'; -const ExampleComponent: FC<{}> = () => { +export const CircleCIPage: FC<{}> = () => { const [token, setToken] = useState(''); return ( @@ -55,8 +55,8 @@ const ExampleComponent: FC<{}> = () => { - - + + @@ -65,4 +65,4 @@ const ExampleComponent: FC<{}> = () => { ); }; -export default ExampleComponent; +export default CircleCIPage; diff --git a/plugins/circleci/src/components/CircleCIPage/index.ts b/plugins/circleci/src/components/CircleCIPage/index.ts new file mode 100644 index 0000000000..188ed6d61f --- /dev/null +++ b/plugins/circleci/src/components/CircleCIPage/index.ts @@ -0,0 +1 @@ +export { CircleCIPage } from './CircleCIPage'; diff --git a/plugins/circleci/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/circleci/src/components/ExampleComponent/ExampleComponent.test.tsx deleted file mode 100644 index ef0f3f9632..0000000000 --- a/plugins/circleci/src/components/ExampleComponent/ExampleComponent.test.tsx +++ /dev/null @@ -1,34 +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 React from 'react'; -import { render } from '@testing-library/react'; -import mockFetch from 'jest-fetch-mock'; -import ExampleComponent from './ExampleComponent'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; - -describe('ExampleComponent', () => { - it('should render', () => { - mockFetch.mockResponse(() => new Promise(() => {})); - const rendered = render( - - - , - ); - expect(rendered.getByText('Welcome to circleci!')).toBeInTheDocument(); - }); -}); diff --git a/plugins/circleci/src/components/ExampleComponent/index.ts b/plugins/circleci/src/components/ExampleComponent/index.ts deleted file mode 100644 index e785d45082..0000000000 --- a/plugins/circleci/src/components/ExampleComponent/index.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 { default } from './ExampleComponent'; diff --git a/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx deleted file mode 100644 index 7fecdc6f11..0000000000 --- a/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx +++ /dev/null @@ -1,28 +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 React from 'react'; -import { render } from '@testing-library/react'; -import mockFetch from 'jest-fetch-mock'; -import ExampleFetchComponent from './ExampleFetchComponent'; - -describe('ExampleFetchComponent', () => { - it('should render', async () => { - mockFetch.mockResponse(() => new Promise(() => {})); - const rendered = render(); - expect(await rendered.findByTestId('progress')).toBeInTheDocument(); - }); -}); diff --git a/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx deleted file mode 100644 index c9bed14117..0000000000 --- a/plugins/circleci/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx +++ /dev/null @@ -1,161 +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 wr iting, 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, { FC, useRef, useEffect } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableContainer from '@material-ui/core/TableContainer'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; -// import Alert from '@material-ui/lab/Alert'; -import { useAsync } from 'react-use'; -// import { Progress } from '@backstage/core'; - -import { CircleCI, GitType, CircleCIOptions, BuildSummary } from 'circleci-api'; - -// const CIRCLECI_TOKEN: string = '943aa82531ccaab192b4c4bc614507dff31c094c'; - -// Configure the factory with some defaults - -// const api = new CircleCI(options); - -// api -// .builds() -// .then(d => console.log('token is valid', d)) -// .catch(() => console.error('invalid token')); - -const useStyles = makeStyles({ - table: { - minWidth: 650, - }, - 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: FC = ({ users }) => { - const classes = useStyles(); - - return ( - - - - - Avatar - Name - Email - Nationality - - - - {users.map(user => ( - - - {user.name.first} - - - {user.name.first} {user.name.last} - - {user.email} - {user.nat} - - ))} - -
-
- ); -}; -const options: Partial = { - // Required for all requests - // token: CIRCLECI_TOKEN, // Set your CircleCi API token - - // Optional - // Anything set here can be overriden when making the request - - // Git information is required for project/build/etc endpoints - vcs: { - type: GitType.GITHUB, // default: github - owner: 'CircleCITest3', - repo: 'circleci-test', - }, - - // Optional query params for requests - // options: { - // branch: "master", // default: master - // } -}; - -const BuildList: React.FC<{ builds: BuildSummary[] }> = ({ builds }) => ( -
    - {builds.map(build => ( -
  • - #{build.build_num} ({build.subject}) -
  • - ))} -
-); -const ExampleFetchComponent: FC<{ token: string }> = ({ token }) => { - const api = useRef(null); - useEffect(() => { - if (token !== '') api.current = new CircleCI({ ...options, token }); - }, [token]); - - const { value, loading, error } = useAsync(() => { - if (api.current) return api.current.builds(); - return Promise.reject('Api token not provided'); - }, [token]); - - if (loading) return
loading
; - if (error) return
{JSON.stringify(error, null, 2)}
; - - return ; -}; - -export default ExampleFetchComponent; diff --git a/plugins/circleci/src/components/ExampleFetchComponent/index.ts b/plugins/circleci/src/components/ExampleFetchComponent/index.ts deleted file mode 100644 index 28482f9fe1..0000000000 --- a/plugins/circleci/src/components/ExampleFetchComponent/index.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 { default } from './ExampleFetchComponent'; diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 4aec241bdb..5c1eca9be3 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -14,11 +14,11 @@ * limitations under the License. */ import { createPlugin } from '@backstage/core'; -import ExampleComponent from './components/ExampleComponent'; +import { CircleCIPage } from './components/CircleCIPage'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { - router.registerRoute('/circleci', ExampleComponent); + router.registerRoute('/circleci', CircleCIPage); }, }); From a0c9155272a8fbf44550471b0de1778d4de9740a Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 30 Apr 2020 11:55:22 +0200 Subject: [PATCH 04/22] feat: build name, status and ID --- .../src/components/CITable/CITable.tsx | 4 ++- .../components/CircleCIFetch/CirleCIFetch.tsx | 25 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/components/CITable/CITable.tsx index 21cb03ce6c..7da9709f68 100644 --- a/plugins/circleci/src/components/CITable/CITable.tsx +++ b/plugins/circleci/src/components/CITable/CITable.tsx @@ -29,7 +29,7 @@ export type CITableBuildInfo = { url: string; }; }; - status: 'success' | 'pending' | 'error'; + status: string; tests?: { total: number; passed: number; @@ -50,6 +50,7 @@ export const CITable: FC<{ + ID Build Source Status @@ -60,6 +61,7 @@ export const CITable: FC<{ {builds.map(build => ( + {build.id} {build.buildName}
diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx index 9ca7f93a0a..fff0c25e50 100644 --- a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -43,11 +43,32 @@ const options: Partial = { // } }; +// "lifecycle" : "finished", // :queued, :scheduled, :not_run, :not_running, :running or :finished +// "outcome" : "failed", // :canceled, :infrastructure_fail, :timedout, :failed, :no_tests or :success + +const makeReadableStatus = (status: string | undefined) => { + if (typeof status === 'undefined') return "" + return ({ + retried: 'Retried', + canceled: 'Canceled', + infrastructure_fail: 'Infra fail', + timedout: 'Timedout', + not_run: 'Not run', + running: 'Running', + failed: 'Failed', + queued: 'Queued', + scheduled: 'Scheduled', + not_running: 'Not running', + no_tests: 'No tests', + fixed: 'Fixed', + success: 'Success', + } as Record)[status]}; + const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { return buildsData.map(buildData => { const tableBuildInfo: CITableBuildInfo = { id: String(buildData.build_num), - buildName: String(buildData.subject), + buildName: buildData.subject ? String(buildData.subject) : '', onRetriggerClick: () => null, source: { branchName: String(buildData.branch), @@ -56,7 +77,7 @@ const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { url: 'todo', }, }, - status: String(buildData.status) as 'success' | 'error' | 'pending', + status: makeReadableStatus(buildData.status), tests: { failed: 0, passed: 10, From 60828973825e1234faa5180846494d73999abf59 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 30 Apr 2020 16:19:00 +0200 Subject: [PATCH 05/22] feat: proper api flow implementation --- packages/app/src/apis.ts | 4 +- plugins/circleci/src/api/index.ts | 61 +++++++++++++++++++ .../components/CircleCIFetch/CirleCIFetch.tsx | 58 ++++++------------ .../components/CircleCIPage/CircleCIPage.tsx | 20 ++---- .../src/components/LoginCard/LoginCard.tsx | 46 ++++++++++++++ .../src/components/LoginCard/index.ts | 1 + plugins/circleci/src/index.ts | 1 + 7 files changed, 137 insertions(+), 54 deletions(-) create mode 100644 plugins/circleci/src/api/index.ts create mode 100644 plugins/circleci/src/components/LoginCard/LoginCard.tsx create mode 100644 plugins/circleci/src/components/LoginCard/index.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index a24a25e205..c00aee1f5c 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -36,6 +36,8 @@ import { loadSampleData, } from '@backstage/plugin-tech-radar'; +import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; + const builder = ApiRegistry.builder(); export const alertApiForwarder = new AlertApiForwarder(); @@ -43,7 +45,7 @@ builder.add(alertApiRef, alertApiForwarder); export const errorApiForwarder = new ErrorApiForwarder(alertApiForwarder); builder.add(errorApiRef, errorApiForwarder); - +builder.add(circleCIApiRef, new CircleCIApi()); builder.add(featureFlagsApiRef, new FeatureFlags()); builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003')); diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts new file mode 100644 index 0000000000..849ac4ce1a --- /dev/null +++ b/plugins/circleci/src/api/index.ts @@ -0,0 +1,61 @@ +/* + * 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 { CircleCI, GitType, CircleCIOptions } from 'circleci-api'; +import { ApiRef } from '@backstage/core'; + +const options: Partial = { + // Required for all requests + // token: CIRCLECI_TOKEN, // Set your CircleCi API token + + // Optional + // Anything set here can be overriden when making the request + + // Git information is required for project/build/etc endpoints + vcs: { + type: GitType.GITHUB, // default: github + owner: 'CircleCITest3', + repo: 'circleci-test', + }, +}; + +export class CircleCIApi { + api: null | CircleCI = null; + constuctor() {} + async authenticate(token: string) { + try { + if (token === '') return Promise.reject(); + this.api = new CircleCI({ ...options, token }); + // await this.api.me(); + return Promise.resolve(); + } catch (e) { + this.api = null; + return this.cantAuth(); + } + } + async cantAuth() { + return Promise.reject("Can't auth"); + } + async getBuilds() { + if (!this.api) return this.cantAuth(); + return this.api.builds(); + } +} + +export const circleCIApiRef = new ApiRef({ + id: 'plugin.circleci.service', + description: 'Used by the CircleCI plugin to make requests', +}); diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx index fff0c25e50..50ca39b3a6 100644 --- a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -14,40 +14,22 @@ * limitations under the License. */ -import React, { FC, useRef, useEffect } from 'react'; +import React, { FC } from 'react'; // import Alert from '@material-ui/lab/Alert'; -import { useAsync } from 'react-use'; // import { Progress } from '@backstage/core'; -import { CircleCI, GitType, CircleCIOptions, BuildSummary } from 'circleci-api'; +import { BuildSummary } from 'circleci-api'; import { CITable, CITableBuildInfo } from '../CITable'; -const options: Partial = { - // Required for all requests - // token: CIRCLECI_TOKEN, // Set your CircleCi API token - - // Optional - // Anything set here can be overriden when making the request - - // Git information is required for project/build/etc endpoints - vcs: { - type: GitType.GITHUB, // default: github - owner: 'CircleCITest3', - repo: 'circleci-test', - }, - - // Optional query params for requests - // options: { - // branch: "master", // default: master - // } -}; +import { circleCIApiRef } from 'api'; +import { useApi } from '@backstage/core'; // "lifecycle" : "finished", // :queued, :scheduled, :not_run, :not_running, :running or :finished // "outcome" : "failed", // :canceled, :infrastructure_fail, :timedout, :failed, :no_tests or :success const makeReadableStatus = (status: string | undefined) => { - if (typeof status === 'undefined') return "" + if (typeof status === 'undefined') return ''; return ({ retried: 'Retried', canceled: 'Canceled', @@ -62,7 +44,8 @@ const makeReadableStatus = (status: string | undefined) => { no_tests: 'No tests', fixed: 'Fixed', success: 'Success', - } as Record)[status]}; + } as Record)[status]; +}; const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { return buildsData.map(buildData => { @@ -90,20 +73,19 @@ const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { }); }; -export const CircleCIFetch: FC<{ token: string }> = ({ token }) => { - const api = useRef(null); - useEffect(() => { - if (token !== '') api.current = new CircleCI({ ...options, token }); - }, [token]); +export const CircleCIFetch: FC<{}> = () => { + const [builds, setBuilds] = React.useState([]); + const api = useApi(circleCIApiRef); - const { value, loading, error } = useAsync(() => { - if (api.current) return api.current.builds(); - return Promise.reject('Api token not provided'); - }, [token]); + React.useEffect(() => { + const intervalId = setInterval(() => { + if (!api.api) return; + api.getBuilds().then(setBuilds); + }, 1500); + return () => clearInterval(intervalId); + }, []); - if (loading) return
loading
; - if (error) return
{JSON.stringify(error, null, 2)}
; - - const builds = transform(value || []); - return ; + if (!api.api) return
Not authenticated
; + const transformedBuilds = transform(builds || []); + return ; }; diff --git a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx index f27ac80a6f..23ac5c0d7d 100644 --- a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx +++ b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import React, { FC, useState } from 'react'; -import { Typography, Grid, Input } from '@material-ui/core'; +import React, { FC } from 'react'; +import { Grid } from '@material-ui/core'; import { InfoCard, Header, @@ -27,10 +27,9 @@ import { SupportButton, } from '@backstage/core'; import { CircleCIFetch } from '../CircleCIFetch'; +import { LoginCard } from '../LoginCard'; export const CircleCIPage: FC<{}> = () => { - const [token, setToken] = useState(''); - return (
@@ -43,20 +42,11 @@ export const CircleCIPage: FC<{}> = () => { - - - Please paste your CircleCI token here - setToken(e.target.value)} - type="password" - value={token} - > - - + - + diff --git a/plugins/circleci/src/components/LoginCard/LoginCard.tsx b/plugins/circleci/src/components/LoginCard/LoginCard.tsx new file mode 100644 index 0000000000..09c0514064 --- /dev/null +++ b/plugins/circleci/src/components/LoginCard/LoginCard.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { + Typography, + Button, + TextField, + List, + ListItem, +} from '@material-ui/core'; +import { Person as PersonIcon } from '@material-ui/icons'; +import { InfoCard, useApi } from '@backstage/core'; +import { circleCIApiRef } from 'api'; + +export const LoginCard = () => { + const [token, setToken] = React.useState(''); + const api = useApi(circleCIApiRef); + + return ( + + + CircleCI Auth + + + + setToken(e.target.value)} + /> + + + + + + + + ); +}; diff --git a/plugins/circleci/src/components/LoginCard/index.ts b/plugins/circleci/src/components/LoginCard/index.ts new file mode 100644 index 0000000000..1d3c3bcf3e --- /dev/null +++ b/plugins/circleci/src/components/LoginCard/index.ts @@ -0,0 +1 @@ +export * from './LoginCard'; diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts index 3a0a0fe2d3..d67bc6a864 100644 --- a/plugins/circleci/src/index.ts +++ b/plugins/circleci/src/index.ts @@ -15,3 +15,4 @@ */ export { plugin } from './plugin'; +export * from './api'; From 00efbfe87ef481b66b4e8b4570344b77cb5c77f5 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 30 Apr 2020 16:37:07 +0200 Subject: [PATCH 06/22] feat: save token to session storage and read from it --- .../src/components/LoginCard/LoginCard.tsx | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/plugins/circleci/src/components/LoginCard/LoginCard.tsx b/plugins/circleci/src/components/LoginCard/LoginCard.tsx index 09c0514064..806f218cf9 100644 --- a/plugins/circleci/src/components/LoginCard/LoginCard.tsx +++ b/plugins/circleci/src/components/LoginCard/LoginCard.tsx @@ -10,10 +10,36 @@ import { Person as PersonIcon } from '@material-ui/icons'; import { InfoCard, useApi } from '@backstage/core'; import { circleCIApiRef } from 'api'; +const useSessionStorage = (key: string): [string, (value: string) => void] => { + const [value, setter] = React.useState(sessionStorage.getItem(key) ?? ''); + const setValue = (newValue: string) => { + sessionStorage.setItem(key, newValue); + setter(sessionStorage.getItem(key) ?? ''); + }; + + React.useEffect(() => { + const storageChangeHandle = (e: StorageEvent) => { + if (e.storageArea !== sessionStorage) return; + if (e.key !== key) return; + if (e.newValue !== e.oldValue) { + setter(e.newValue ?? ''); + } + }; + window.addEventListener('storage', storageChangeHandle); + return () => window.removeEventListener('storage', storageChangeHandle); + }, [key, setter]); + + return [value, setValue]; +}; export const LoginCard = () => { - const [token, setToken] = React.useState(''); + const [token, setToken] = useSessionStorage(circleCIApiRef.id); const api = useApi(circleCIApiRef); + React.useEffect(() => { + if (token && token !== '') { + api.authenticate(token); + } + }, []); return ( From cf963ceae8258e00abda8730324f3f4139649d21 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Mon, 4 May 2020 13:46:15 +0200 Subject: [PATCH 07/22] Add owner and repo options to authenticate method --- plugins/circleci/src/api/index.ts | 25 +++++++++++------- .../src/components/LoginCard/LoginCard.tsx | 26 ++++++++++++++++--- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 849ac4ce1a..aadb2b144d 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -14,8 +14,15 @@ * limitations under the License. */ -import { CircleCI, GitType, CircleCIOptions } from 'circleci-api'; +import { CircleCI, GitType, CircleCIOptions, GitInfo } from 'circleci-api'; import { ApiRef } from '@backstage/core'; +import { default } from '../../../../packages/core/src/components/Status/Status.stories'; + +const defaultVcsOptions: GitInfo = { + type: GitType.GITHUB, // default: github + owner: 'CircleCITest3', + repo: 'circleci-test', +} const options: Partial = { // Required for all requests @@ -25,20 +32,20 @@ const options: Partial = { // Anything set here can be overriden when making the request // Git information is required for project/build/etc endpoints - vcs: { - type: GitType.GITHUB, // default: github - owner: 'CircleCITest3', - repo: 'circleci-test', - }, + vcs: defaultVcsOptions }; export class CircleCIApi { api: null | CircleCI = null; constuctor() {} - async authenticate(token: string) { + async authenticate({token, owner, repo}: {token: string, owner: string, repo: string}) { try { - if (token === '') return Promise.reject(); - this.api = new CircleCI({ ...options, token }); + if (token === '' || owner === '' || repo === '') return Promise.reject(); + this.api = new CircleCI({ ...options, token, vcs: { + type: GitType.GITHUB, // default: github + owner, + repo, + }}); // await this.api.me(); return Promise.resolve(); } catch (e) { diff --git a/plugins/circleci/src/components/LoginCard/LoginCard.tsx b/plugins/circleci/src/components/LoginCard/LoginCard.tsx index 806f218cf9..e50ca4bc0b 100644 --- a/plugins/circleci/src/components/LoginCard/LoginCard.tsx +++ b/plugins/circleci/src/components/LoginCard/LoginCard.tsx @@ -32,12 +32,15 @@ const useSessionStorage = (key: string): [string, (value: string) => void] => { return [value, setValue]; }; export const LoginCard = () => { - const [token, setToken] = useSessionStorage(circleCIApiRef.id); + const [token, setToken] = useSessionStorage(circleCIApiRef.id + '_token'); + const [owner, setOwner] = useSessionStorage(circleCIApiRef.id + '_owner'); + const [repo, setRepo] = useSessionStorage(circleCIApiRef.id + '_repo'); + const api = useApi(circleCIApiRef); React.useEffect(() => { if (token && token !== '') { - api.authenticate(token); + api.authenticate({token, owner, repo}); } }, []); return ( @@ -55,13 +58,28 @@ export const LoginCard = () => { onChange={e => setToken(e.target.value)} /> - + + setOwner(e.target.value)} + /> + + + setRepo(e.target.value)} + /> + From f7088027118212bf6f41488afb72cdad1990b746 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Mon, 4 May 2020 14:53:01 +0200 Subject: [PATCH 08/22] Move owner/repo form into CircleCIFetch component --- plugins/circleci/src/api/index.ts | 21 +++++----- .../src/components/CITable/CITable.tsx | 1 - .../components/CircleCIFetch/CirleCIFetch.tsx | 14 +++++-- .../src/components/LoginCard/LoginCard.tsx | 22 +--------- .../components/ProjectInput/ProjectInput.tsx | 42 +++++++++++++++++++ 5 files changed, 64 insertions(+), 36 deletions(-) create mode 100644 plugins/circleci/src/components/ProjectInput/ProjectInput.tsx diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index aadb2b144d..34eaae4b35 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { CircleCI, GitType, CircleCIOptions, GitInfo } from 'circleci-api'; +import { CircleCI, GitType, CircleCIOptions, GitInfo, getBuildSummaries } from 'circleci-api'; import { ApiRef } from '@backstage/core'; -import { default } from '../../../../packages/core/src/components/Status/Status.stories'; +//import { default } from '../../../../packages/core/src/components/Status/Status.stories'; const defaultVcsOptions: GitInfo = { type: GitType.GITHUB, // default: github @@ -37,16 +37,14 @@ const options: Partial = { export class CircleCIApi { api: null | CircleCI = null; + token: string = ''; constuctor() {} - async authenticate({token, owner, repo}: {token: string, owner: string, repo: string}) { + async authenticate(token: string) { try { - if (token === '' || owner === '' || repo === '') return Promise.reject(); - this.api = new CircleCI({ ...options, token, vcs: { - type: GitType.GITHUB, // default: github - owner, - repo, - }}); + if (token === '') return Promise.reject(); + this.api = new CircleCI({ ...options, token}); // await this.api.me(); + this.token = token; return Promise.resolve(); } catch (e) { this.api = null; @@ -56,9 +54,10 @@ export class CircleCIApi { async cantAuth() { return Promise.reject("Can't auth"); } - async getBuilds() { + async getBuilds({repo, owner}: {repo: string, owner: string}) { if (!this.api) return this.cantAuth(); - return this.api.builds(); + if (owner === '' || repo === '') return Promise.reject(); + return getBuildSummaries(this.token, {vcs: {...defaultVcsOptions, owner, repo}}); } } diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/components/CITable/CITable.tsx index 7da9709f68..a6b607a216 100644 --- a/plugins/circleci/src/components/CITable/CITable.tsx +++ b/plugins/circleci/src/components/CITable/CITable.tsx @@ -7,7 +7,6 @@ import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; - const useStyles = makeStyles({ table: { minWidth: 650, diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx index 50ca39b3a6..855fea6243 100644 --- a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; // import Alert from '@material-ui/lab/Alert'; // import { Progress } from '@backstage/core'; @@ -24,6 +24,7 @@ import { BuildSummary } from 'circleci-api'; import { CITable, CITableBuildInfo } from '../CITable'; import { circleCIApiRef } from 'api'; import { useApi } from '@backstage/core'; +import { ProjectInput } from 'components/ProjectInput/ProjectInput'; // "lifecycle" : "finished", // :queued, :scheduled, :not_run, :not_running, :running or :finished // "outcome" : "failed", // :canceled, :infrastructure_fail, :timedout, :failed, :no_tests or :success @@ -73,19 +74,24 @@ const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { }); }; + export const CircleCIFetch: FC<{}> = () => { + const [vcsOptions, setVcsOptions] = useState({owner: '', repo: ''}); const [builds, setBuilds] = React.useState([]); const api = useApi(circleCIApiRef); React.useEffect(() => { const intervalId = setInterval(() => { if (!api.api) return; - api.getBuilds().then(setBuilds); + api.getBuilds(vcsOptions).then(setBuilds); }, 1500); return () => clearInterval(intervalId); }, []); - if (!api.api) return
Not authenticated
; const transformedBuilds = transform(builds || []); - return ; + return <> + { + setVcsOptions(info); + api.getBuilds(info).then(setBuilds)}}/> + {!api.api ?
Not authenticated
: }; }; diff --git a/plugins/circleci/src/components/LoginCard/LoginCard.tsx b/plugins/circleci/src/components/LoginCard/LoginCard.tsx index e50ca4bc0b..23ff5ee1d1 100644 --- a/plugins/circleci/src/components/LoginCard/LoginCard.tsx +++ b/plugins/circleci/src/components/LoginCard/LoginCard.tsx @@ -33,14 +33,12 @@ const useSessionStorage = (key: string): [string, (value: string) => void] => { }; export const LoginCard = () => { const [token, setToken] = useSessionStorage(circleCIApiRef.id + '_token'); - const [owner, setOwner] = useSessionStorage(circleCIApiRef.id + '_owner'); - const [repo, setRepo] = useSessionStorage(circleCIApiRef.id + '_repo'); const api = useApi(circleCIApiRef); React.useEffect(() => { if (token && token !== '') { - api.authenticate({token, owner, repo}); + api.authenticate(token); } }, []); return ( @@ -58,28 +56,12 @@ export const LoginCard = () => { onChange={e => setToken(e.target.value)} />
- - setOwner(e.target.value)} - /> - - - setRepo(e.target.value)} - /> - diff --git a/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx b/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx new file mode 100644 index 0000000000..79ca532f70 --- /dev/null +++ b/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx @@ -0,0 +1,42 @@ +import { useState, FC } from "react"; +import { List, ListItem, TextField, Button } from "@material-ui/core"; +import React from "react"; + +export const ProjectInput:FC<{ + setGitInfo: (info: {owner: string, repo: string}) => void + }> = ({setGitInfo}) => { + + const [owner, setOwner] = useState(''); + const [repo, setRepo] = useState(''); + + return ( + + + setOwner(e.target.value)} + /> + + + setRepo(e.target.value)} + /> + + + + + + ); +}; \ No newline at end of file From a6dcc9b2e31c2dc17a2902382ca28033a4f23ffd Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 4 May 2020 15:48:07 +0200 Subject: [PATCH 09/22] feat: settings page --- plugins/circleci/src/api/index.ts | 90 ++++++++++++------- .../components/CircleCIFetch/CirleCIFetch.tsx | 17 +++- .../components/CircleCIPage/CircleCIPage.tsx | 11 +-- .../src/components/LoginCard/LoginCard.tsx | 72 --------------- .../src/components/LoginCard/index.ts | 1 - .../components/SettingsPage/SettingsPage.tsx | 83 +++++++++++++++++ .../src/components/SettingsPage/index.ts | 1 + plugins/circleci/src/plugin.ts | 2 + 8 files changed, 160 insertions(+), 117 deletions(-) delete mode 100644 plugins/circleci/src/components/LoginCard/LoginCard.tsx delete mode 100644 plugins/circleci/src/components/LoginCard/index.ts create mode 100644 plugins/circleci/src/components/SettingsPage/SettingsPage.tsx create mode 100644 plugins/circleci/src/components/SettingsPage/index.ts diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 849ac4ce1a..b3de6758cc 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -17,45 +17,69 @@ import { CircleCI, GitType, CircleCIOptions } from 'circleci-api'; import { ApiRef } from '@backstage/core'; -const options: Partial = { - // Required for all requests - // token: CIRCLECI_TOKEN, // Set your CircleCi API token - - // Optional - // Anything set here can be overriden when making the request - - // Git information is required for project/build/etc endpoints +const defaultOptions: Partial = { vcs: { - type: GitType.GITHUB, // default: github + type: GitType.GITHUB, owner: 'CircleCITest3', repo: 'circleci-test', }, }; - -export class CircleCIApi { - api: null | CircleCI = null; - constuctor() {} - async authenticate(token: string) { - try { - if (token === '') return Promise.reject(); - this.api = new CircleCI({ ...options, token }); - // await this.api.me(); - return Promise.resolve(); - } catch (e) { - this.api = null; - return this.cantAuth(); - } - } - async cantAuth() { - return Promise.reject("Can't auth"); - } - async getBuilds() { - if (!this.api) return this.cantAuth(); - return this.api.builds(); - } -} - export const circleCIApiRef = new ApiRef({ id: 'plugin.circleci.service', description: 'Used by the CircleCI plugin to make requests', }); + +export class CircleCIApi { + token: string = ''; + private options: Partial; + + authed: boolean = false; + constructor(options?: Partial) { + this.options = Object.assign(Object.create(null), defaultOptions, options); + } + + setToken(token: string) { + this.token = token; + this.persistToken(); + } + + async restorePersistedToken() { + if (this.authed) return Promise.resolve(); + const key = circleCIApiRef.id; + const persistedToken = sessionStorage.getItem(key); + if (persistedToken) { + this.token = persistedToken; + return Promise.resolve(); + } + return Promise.reject(); + } + + async persistToken() { + if (this.authed) return; + const key = circleCIApiRef.id; + sessionStorage.setItem(key, this.token); + } + + async validateToken() { + if (!this.token || this.token === '') { + return Promise.reject('Wrong token'); + } + + // TODO: switch towards using personal token + await this.api.builds(); + this.authed = true; + return Promise.resolve(); + } + + private get api() { + return new CircleCI({ ...this.options, token: this.token }); + } + + async getBuilds() { + return this.api.builds(); + } + + async getUser() { + return this.api.me(); + } +} diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx index 50ca39b3a6..6e27d3e51e 100644 --- a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -74,18 +74,27 @@ const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { }; export const CircleCIFetch: FC<{}> = () => { + const [authed, setAuthed] = React.useState(false); const [builds, setBuilds] = React.useState([]); const api = useApi(circleCIApiRef); React.useEffect(() => { - const intervalId = setInterval(() => { - if (!api.api) return; + const intervalId = setInterval(async () => { + if (!authed) { + await api.restorePersistedToken(); + await api + .validateToken() + .then(() => { + setAuthed(true); + }) + .catch(() => setAuthed(false)); + } api.getBuilds().then(setBuilds); }, 1500); return () => clearInterval(intervalId); - }, []); + }, [authed]); - if (!api.api) return
Not authenticated
; + if (!authed) return
Not authenticated
; const transformedBuilds = transform(builds || []); return ; }; diff --git a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx index 23ac5c0d7d..a37d5e633d 100644 --- a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx +++ b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx @@ -15,7 +15,7 @@ */ import React, { FC } from 'react'; -import { Grid } from '@material-ui/core'; +import { Grid, Button } from '@material-ui/core'; import { InfoCard, Header, @@ -27,7 +27,6 @@ import { SupportButton, } from '@backstage/core'; import { CircleCIFetch } from '../CircleCIFetch'; -import { LoginCard } from '../LoginCard'; export const CircleCIPage: FC<{}> = () => { return ( @@ -37,15 +36,13 @@ export const CircleCIPage: FC<{}> = () => {
- + + A description of your plugin goes here. - - - - + diff --git a/plugins/circleci/src/components/LoginCard/LoginCard.tsx b/plugins/circleci/src/components/LoginCard/LoginCard.tsx deleted file mode 100644 index 806f218cf9..0000000000 --- a/plugins/circleci/src/components/LoginCard/LoginCard.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { - Typography, - Button, - TextField, - List, - ListItem, -} from '@material-ui/core'; -import { Person as PersonIcon } from '@material-ui/icons'; -import { InfoCard, useApi } from '@backstage/core'; -import { circleCIApiRef } from 'api'; - -const useSessionStorage = (key: string): [string, (value: string) => void] => { - const [value, setter] = React.useState(sessionStorage.getItem(key) ?? ''); - const setValue = (newValue: string) => { - sessionStorage.setItem(key, newValue); - setter(sessionStorage.getItem(key) ?? ''); - }; - - React.useEffect(() => { - const storageChangeHandle = (e: StorageEvent) => { - if (e.storageArea !== sessionStorage) return; - if (e.key !== key) return; - if (e.newValue !== e.oldValue) { - setter(e.newValue ?? ''); - } - }; - window.addEventListener('storage', storageChangeHandle); - return () => window.removeEventListener('storage', storageChangeHandle); - }, [key, setter]); - - return [value, setValue]; -}; -export const LoginCard = () => { - const [token, setToken] = useSessionStorage(circleCIApiRef.id); - const api = useApi(circleCIApiRef); - - React.useEffect(() => { - if (token && token !== '') { - api.authenticate(token); - } - }, []); - return ( - - - CircleCI Auth - - - - setToken(e.target.value)} - /> - - - - - - - - ); -}; diff --git a/plugins/circleci/src/components/LoginCard/index.ts b/plugins/circleci/src/components/LoginCard/index.ts deleted file mode 100644 index 1d3c3bcf3e..0000000000 --- a/plugins/circleci/src/components/LoginCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './LoginCard'; diff --git a/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx new file mode 100644 index 0000000000..5b4487bb32 --- /dev/null +++ b/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Button, TextField, List, Grid, ListItem } from '@material-ui/core'; +import { circleCIApiRef } from 'api'; +import { + InfoCard, + useApi, + Header, + Page, + pageTheme, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core'; + +export const SettingsPage = () => { + const [authed, setAuthed] = React.useState(false); + const [token, setToken] = React.useState(''); + const api = useApi(circleCIApiRef); + + React.useEffect(() => { + api + .restorePersistedToken() + .then(() => api.validateToken()) + .then(() => setAuthed(true)) + .catch(() => setAuthed(false)); + }, []); + + return ( + +
+ + +
+ + + + A description of your plugin goes here. + + + + + + {authed ? ( + <>Already authed + ) : ( + <> + + setToken(e.target.value)} + /> + + + + + + + )} + + + + + +
+ ); +}; diff --git a/plugins/circleci/src/components/SettingsPage/index.ts b/plugins/circleci/src/components/SettingsPage/index.ts new file mode 100644 index 0000000000..f533f5abe0 --- /dev/null +++ b/plugins/circleci/src/components/SettingsPage/index.ts @@ -0,0 +1 @@ +export * from './SettingsPage'; diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 5c1eca9be3..458a7f27bc 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -15,10 +15,12 @@ */ import { createPlugin } from '@backstage/core'; import { CircleCIPage } from './components/CircleCIPage'; +import { SettingsPage } from './components/SettingsPage'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { router.registerRoute('/circleci', CircleCIPage); + router.registerRoute('/circleci/settings', SettingsPage); }, }); From 31d05b1f922ce0b7ea0fe03b40a96a8ca1e8b75b Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 4 May 2020 20:58:39 +0200 Subject: [PATCH 10/22] feat: circleCI backend proxy --- packages/app/package.json | 1 + packages/app/src/plugins.ts | 1 + packages/backend/package.json | 1 + packages/backend/src/index.ts | 2 ++ plugins/circleci-backend/.eslintrc.js | 3 +++ plugins/circleci-backend/README.md | 6 ++++++ plugins/circleci-backend/package.json | 23 ++++++++++++++++++++++ plugins/circleci-backend/src/index.ts | 27 ++++++++++++++++++++++++++ plugins/circleci-backend/tsconfig.json | 15 ++++++++++++++ plugins/circleci/src/api/index.ts | 11 +++++------ yarn.lock | 15 ++++++++++++-- 11 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 plugins/circleci-backend/.eslintrc.js create mode 100644 plugins/circleci-backend/README.md create mode 100644 plugins/circleci-backend/package.json create mode 100644 plugins/circleci-backend/src/index.ts create mode 100644 plugins/circleci-backend/tsconfig.json diff --git a/packages/app/package.json b/packages/app/package.json index da1d02a5be..e88bd46a54 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -21,6 +21,7 @@ "@backstage/cli": "^0.1.1-alpha.4", "@backstage/core": "^0.1.1-alpha.4", "@backstage/plugin-circleci": "^0.1.1-alpha.4", + "@backstage/plugin-circleci-backend": "^0.1.1-alpha.4", "@backstage/plugin-home-page": "^0.1.1-alpha.4", "@backstage/plugin-inventory": "^0.1.1-alpha.4", "@backstage/plugin-lighthouse": "^0.1.1-alpha.4", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 31546c6ba6..b46bf2ace8 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -19,3 +19,4 @@ export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse'; export { plugin as InventoryPlugin } from '@backstage/plugin-inventory'; export { plugin as TechRadar } from '@backstage/plugin-tech-radar'; export { plugin as Circleci } from '@backstage/plugin-circleci'; +// export { plugin as CircleciBackend } from '@backstage/plugin-circleci-backend'; diff --git a/packages/backend/package.json b/packages/backend/package.json index 0530f04452..13d64a0a13 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@backstage/plugin-inventory-backend": "0.1.1-alpha.4", + "@backstage/plugin-circleci-backend": "0.1.1-alpha.4", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index fd8015c328..da1a4f3dc3 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -28,6 +28,7 @@ import helmet from 'helmet'; import compression from 'compression'; import { testRouter } from './test'; import { router as inventoryRouter } from '@backstage/plugin-inventory-backend'; +import { router as circleCIRouter } from '@backstage/plugin-circleci-backend'; const DEFAULT_PORT = 7000; @@ -40,6 +41,7 @@ app.use(compression()); app.use(express.json()); app.use('/test', testRouter); app.use('/inventory', inventoryRouter); +app.use('/circleci', circleCIRouter); app.listen(PORT, () => { console.log(`Listening on port ${PORT}`); diff --git a/plugins/circleci-backend/.eslintrc.js b/plugins/circleci-backend/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/circleci-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/circleci-backend/README.md b/plugins/circleci-backend/README.md new file mode 100644 index 0000000000..91e60e3e6b --- /dev/null +++ b/plugins/circleci-backend/README.md @@ -0,0 +1,6 @@ +# Title +Welcome to the circleci-backend plugin! + +## Sub-section 1 + +## Sub-section 2 diff --git a/plugins/circleci-backend/package.json b/plugins/circleci-backend/package.json new file mode 100644 index 0000000000..6535deba10 --- /dev/null +++ b/plugins/circleci-backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "@backstage/plugin-circleci-backend", + "version": "0.1.1-alpha.4", + "main": "dist", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "tsc", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "clean": "backstage-cli clean" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.4" + }, + "dependencies": { + "express": "^4.17.1", + "http-proxy-middleware": "^1.0.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/circleci-backend/src/index.ts b/plugins/circleci-backend/src/index.ts new file mode 100644 index 0000000000..45bf47b884 --- /dev/null +++ b/plugins/circleci-backend/src/index.ts @@ -0,0 +1,27 @@ +/* + * 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 express from 'express'; +import httpProxy from 'http-proxy'; + +// Simple proxy for handling CORS for now +const proxy = httpProxy.createServer({ + target: 'https://circleci.com/api/v1.1', + changeOrigin: true, +}); +proxy.on('error', e => console.error(e)); +export const router = express(); +router.use('/api', (req, res) => proxy.web(req, res)); diff --git a/plugins/circleci-backend/tsconfig.json b/plugins/circleci-backend/tsconfig.json new file mode 100644 index 0000000000..b463ac102f --- /dev/null +++ b/plugins/circleci-backend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": ["src"], + "compilerOptions": { + "baseUrl": "src", + "outDir": "dist", + "incremental": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "target": "es5", + "module": "commonjs", + "esModuleInterop": true, + "types": ["node", "jest"] + } +} diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 70068f29ba..d42023c657 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -18,6 +18,7 @@ import { CircleCI, GitType, CircleCIOptions } from 'circleci-api'; import { ApiRef } from '@backstage/core'; const defaultOptions: Partial = { + circleHost: 'http://backstage.localhost:7000/circleci/api', vcs: { type: GitType.GITHUB, owner: 'CircleCITest3', @@ -59,10 +60,10 @@ export class CircleCIApi { const persistedToken = sessionStorage.getItem(key); let persistedVCSOptions: {} | undefined; try { - persistedVCSOptions = JSON.parse(sessionStorage.getItem(key + '_options') as string); - } catch(e) { - - } + persistedVCSOptions = JSON.parse( + sessionStorage.getItem(key + '_options') as string, + ); + } catch (e) {} if (persistedToken && persistedVCSOptions) { this.token = persistedToken; this.options.vcs = persistedVCSOptions; @@ -71,8 +72,6 @@ export class CircleCIApi { return Promise.reject(); } - - async persistToken() { if (this.authed) return; const key = circleCIApiRef.id; diff --git a/yarn.lock b/yarn.lock index cd2016d0d0..b3b8467ce5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4003,7 +4003,7 @@ "@types/http-proxy" "*" "@types/node" "*" -"@types/http-proxy@*": +"@types/http-proxy@*", "@types/http-proxy@^1.17.3": version "1.17.4" resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b" integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q== @@ -10759,7 +10759,18 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy@^1.17.0: +http-proxy-middleware@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.3.tgz#f73daad8dac622d51fe1769960c914b9b1f75a72" + integrity sha512-GHvPeBD+A357zS5tHjzj6ISrVOjjCiy0I92bdyTJz0pNmIjFxO0NX/bX+xkGgnclKQE/5hHAB9JEQ7u9Pw4olg== + dependencies: + "@types/http-proxy" "^1.17.3" + http-proxy "^1.18.0" + is-glob "^4.0.1" + lodash "^4.17.15" + micromatch "^4.0.2" + +http-proxy@^1.17.0, http-proxy@^1.18.0: version "1.18.0" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== From f028801f39602cb82eed6ea8ca67b50e2ca1a5de Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 4 May 2020 21:41:38 +0200 Subject: [PATCH 11/22] feat: retry --- plugins/circleci/src/api/index.ts | 4 ++ .../src/components/CITable/CITable.tsx | 37 +++++++++++++++++-- .../components/CircleCIFetch/CirleCIFetch.tsx | 22 +++++++---- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index d42023c657..93916f82bb 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -93,6 +93,10 @@ export class CircleCIApi { return new CircleCI({ ...this.options, token: this.token }); } + async retry(buildId: string) { + return this.api.retry(Number(buildId)); + } + async getBuilds() { return this.api.builds(); } diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/components/CITable/CITable.tsx index a6b607a216..0e867f8b2c 100644 --- a/plugins/circleci/src/components/CITable/CITable.tsx +++ b/plugins/circleci/src/components/CITable/CITable.tsx @@ -7,6 +7,15 @@ import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; +import Button from '@material-ui/core/Button'; +import { + StatusRunning, + StatusFailed, + StatusOK, + StatusPending, + StatusNA, +} from '@backstage/core'; + const useStyles = makeStyles({ table: { minWidth: 650, @@ -36,7 +45,25 @@ export type CITableBuildInfo = { failed: number; testUrl: string; //fixme better name }; - onRetriggerClick: () => void; + onRetryClick: () => void; +}; + +// :retried, :canceled, :infrastructure_fail, :timedout, :not_run, :running, :failed, :queued, :scheduled, :not_running, :no_tests, :fixed, :success +const getStatusComponent = (status: string) => { + switch (status.toLowerCase()) { + case 'queued': + case 'scheduled': + return ; + case 'running': + return ; + case 'failed': + return ; + case 'success': + return ; + case 'canceled': + default: + return ; + } }; export const CITable: FC<{ @@ -54,7 +81,7 @@ export const CITable: FC<{ Source Status Tests - Retrigger + Actions @@ -68,7 +95,7 @@ export const CITable: FC<{
{build.source.commit.hash}
- {build.status} + {getStatusComponent(build.status)} {build.tests && ( <> @@ -78,7 +105,9 @@ export const CITable: FC<{ )} - Retrigger + + +
))}
diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx index 71d0e0c29b..226b995a72 100644 --- a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -47,12 +47,15 @@ const makeReadableStatus = (status: string | undefined) => { } as Record)[status]; }; -const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { +const transform = ( + buildsData: BuildSummary[], + api: typeof circleCIApiRef.T, +): CITableBuildInfo[] => { return buildsData.map(buildData => { const tableBuildInfo: CITableBuildInfo = { id: String(buildData.build_num), buildName: buildData.subject ? String(buildData.subject) : '', - onRetriggerClick: () => null, + onRetryClick: () => api.retry(String(buildData.build_num)), source: { branchName: String(buildData.branch), commit: { @@ -73,7 +76,6 @@ const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { }); }; - export const CircleCIFetch: FC<{}> = () => { const [authed, setAuthed] = React.useState(false); const [builds, setBuilds] = React.useState([]); @@ -96,8 +98,14 @@ export const CircleCIFetch: FC<{}> = () => { }, [authed]); if (!authed) return
Not authenticated
; - const transformedBuilds = transform(builds || []); - return <> - - {!api.authed ?
Not authenticated
: }; + const transformedBuilds = transform(builds || [], api); + return ( + <> + {!api.authed ? ( +
Not authenticated
+ ) : ( + + )} + + ); }; From 08a3033942a6d3516305333b5d9166e377ac3330 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 4 May 2020 22:42:48 +0200 Subject: [PATCH 12/22] feat: react-router links, some ui edits --- plugins/circleci/package.json | 1 + .../src/components/CITable/CITable.tsx | 54 +++++++++++++------ .../components/CircleCIFetch/CirleCIFetch.tsx | 6 ++- .../components/CircleCIPage/CircleCIPage.tsx | 52 +++++++++++------- .../components/SettingsPage/SettingsPage.tsx | 13 ++--- plugins/circleci/src/plugin.ts | 4 +- 6 files changed, 84 insertions(+), 46 deletions(-) diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 80603f4029..73b10d4fa5 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -30,6 +30,7 @@ "circleci-api": "^4.0.0", "react": "16.13.1", "react-dom": "16.13.1", + "react-router": "^5.1.2", "react-use": "^13.0.0" }, "files": [ diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/components/CITable/CITable.tsx index 0e867f8b2c..882f52fbe0 100644 --- a/plugins/circleci/src/components/CITable/CITable.tsx +++ b/plugins/circleci/src/components/CITable/CITable.tsx @@ -1,15 +1,22 @@ // Idea for this component to be somehow reusable representation of CI table view import React, { FC } from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableContainer from '@material-ui/core/TableContainer'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; -import Button from '@material-ui/core/Button'; import { - StatusRunning, + Button, + Table, + TableBody, + TableCell, + TableHead, + TableContainer, + TableRow, + Link, + CircularProgress, + List, + ListItem, + ListItemText, +} from '@material-ui/core'; +import { Replay as RetryIcon } from '@material-ui/icons'; +import { StatusFailed, StatusOK, StatusPending, @@ -30,6 +37,7 @@ const useStyles = makeStyles({ export type CITableBuildInfo = { id: string; buildName: string; + buildUrl?: string; source: { branchName: string; commit: { @@ -55,7 +63,7 @@ const getStatusComponent = (status: string) => { case 'scheduled': return ; case 'running': - return ; + return ; case 'failed': return ; case 'success': @@ -79,7 +87,7 @@ export const CITable: FC<{ ID Build Source - Status + Status Tests Actions
@@ -88,14 +96,24 @@ export const CITable: FC<{ {builds.map(build => ( {build.id} - {build.buildName} -
-
{build.source.branchName}
-
{build.source.commit.hash}
-
+ + {build.buildName} + +
+ + + + + + + + + + + + {getStatusComponent(build.status)} - {getStatusComponent(build.status)} {build.tests && ( <> @@ -106,7 +124,9 @@ export const CITable: FC<{ )} - +
))} diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx index 226b995a72..59c667182f 100644 --- a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -54,7 +54,10 @@ const transform = ( return buildsData.map(buildData => { const tableBuildInfo: CITableBuildInfo = { id: String(buildData.build_num), - buildName: buildData.subject ? String(buildData.subject) : '', + buildName: buildData.subject + ? buildData.subject + + (buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '') + : '', onRetryClick: () => api.retry(String(buildData.build_num)), source: { branchName: String(buildData.branch), @@ -64,6 +67,7 @@ const transform = ( }, }, status: makeReadableStatus(buildData.status), + buildUrl: buildData.build_url, tests: { failed: 0, passed: 10, diff --git a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx index a37d5e633d..32812b1ade 100644 --- a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx +++ b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx @@ -15,7 +15,10 @@ */ import React, { FC } from 'react'; +import { Route } from 'react-router'; +import { Link as RouterLink } from 'react-router-dom'; import { Grid, Button } from '@material-ui/core'; +import { Settings as SettingsIcon } from '@material-ui/icons'; import { InfoCard, Header, @@ -27,28 +30,39 @@ import { SupportButton, } from '@backstage/core'; import { CircleCIFetch } from '../CircleCIFetch'; - +import { SettingsPage } from '../SettingsPage'; export const CircleCIPage: FC<{}> = () => { return ( - -
- - -
- - - - A description of your plugin goes here. - - - - - - + <> + + +
+ + +
+ + + + + A description of your plugin goes here. + + + + + + + + -
-
-
+ + + ); }; diff --git a/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx index ea3c8fb3a6..4c377f090c 100644 --- a/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx @@ -13,12 +13,12 @@ import { SupportButton, } from '@backstage/core'; import { ProjectInput } from 'components/ProjectInput/ProjectInput'; +import { Link as RouterLink } from 'react-router-dom'; export const SettingsPage = () => { - const [authed, setAuthed] = React.useState(false); - const [token, setToken] = React.useState(''); - const api = useApi(circleCIApiRef); + const [authed, setAuthed] = React.useState(api.authed); + const [token, setToken] = React.useState(''); React.useEffect(() => { api @@ -36,7 +36,9 @@ export const SettingsPage = () => { - + A description of your plugin goes here. @@ -77,8 +79,7 @@ export const SettingsPage = () => { )} - - api.setVCSOptions(info)}/> + api.setVCSOptions(info)} /> diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 458a7f27bc..05d3710d40 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -15,12 +15,10 @@ */ import { createPlugin } from '@backstage/core'; import { CircleCIPage } from './components/CircleCIPage'; -import { SettingsPage } from './components/SettingsPage'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { - router.registerRoute('/circleci', CircleCIPage); - router.registerRoute('/circleci/settings', SettingsPage); + router.registerRoute('/circleci', CircleCIPage, { exact: false }); }, }); From 21ac45698a8804ce5b74857c22090097f1845699 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 4 May 2020 23:10:14 +0200 Subject: [PATCH 13/22] feat: visual adjustments --- plugins/circleci/src/api/index.ts | 4 +- .../src/components/CITable/CITable.tsx | 42 ++++++++----------- .../components/CircleCIFetch/CirleCIFetch.tsx | 14 +++---- .../components/ProjectInput/ProjectInput.tsx | 26 +++++++----- .../components/SettingsPage/SettingsPage.tsx | 13 ++++-- 5 files changed, 53 insertions(+), 46 deletions(-) diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 93916f82bb..955daf3fba 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -31,8 +31,8 @@ export const circleCIApiRef = new ApiRef({ }); export class CircleCIApi { - token: string = ''; - private options: Partial; + private token: string = ''; + options: Partial; authed: boolean = false; constructor(options?: Partial) { diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/components/CITable/CITable.tsx index 882f52fbe0..5efb0edb9f 100644 --- a/plugins/circleci/src/components/CITable/CITable.tsx +++ b/plugins/circleci/src/components/CITable/CITable.tsx @@ -11,9 +11,6 @@ import { TableRow, Link, CircularProgress, - List, - ListItem, - ListItemText, } from '@material-ui/core'; import { Replay as RetryIcon } from '@material-ui/icons'; import { @@ -78,7 +75,7 @@ export const CITable: FC<{ builds: CITableBuildInfo[]; }> = ({ builds }) => { const classes = useStyles(); - + const isTestDataAvailable = builds.some(build => build.tests); return (
@@ -88,8 +85,8 @@ export const CITable: FC<{ BuildSourceStatus - Tests - Actions + {isTestDataAvailable && Tests} + Actions @@ -102,28 +99,25 @@ export const CITable: FC<{ - - - - - - - - + {build.source.branchName} +
+ {build.source.commit.hash}
{getStatusComponent(build.status)} - - {build.tests && ( - <> - {build.tests.passed}/{build.tests.total} ( - {build.tests.failed ? build.tests.failed + ', ' : ''} - {build.tests.skipped ? build.tests.skipped : ''}) - - )} - - + {build.tests && ( + + { + <> + {build.tests.passed}/{build.tests.total} ( + {build.tests.failed ? build.tests.failed + ', ' : ''} + {build.tests.skipped ? build.tests.skipped : ''}) + + } + + )} + diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx index 59c667182f..92d4ca5ce2 100644 --- a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -68,13 +68,13 @@ const transform = ( }, status: makeReadableStatus(buildData.status), buildUrl: buildData.build_url, - tests: { - failed: 0, - passed: 10, - skipped: 3, - testUrl: 'nourlnow', - total: 13, - }, + // tests: { + // failed: 0, + // passed: 10, + // skipped: 3, + // testUrl: 'nourlnow', + // total: 13, + // }, }; return tableBuildInfo; }); diff --git a/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx b/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx index 79ca532f70..046cd171b9 100644 --- a/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx +++ b/plugins/circleci/src/components/ProjectInput/ProjectInput.tsx @@ -1,14 +1,20 @@ -import { useState, FC } from "react"; -import { List, ListItem, TextField, Button } from "@material-ui/core"; -import React from "react"; +import { useState, FC, useEffect } from 'react'; +import { List, ListItem, TextField, Button } from '@material-ui/core'; +import React from 'react'; -export const ProjectInput:FC<{ - setGitInfo: (info: {owner: string, repo: string}) => void - }> = ({setGitInfo}) => { - +export const ProjectInput: FC<{ + setGitInfo: (info: { owner: string; repo: string }) => void; + apiGitInfo?: { owner?: string; repo?: string }; +}> = ({ setGitInfo, apiGitInfo = {} }) => { const [owner, setOwner] = useState(''); const [repo, setRepo] = useState(''); - + + useEffect(() => { + if (apiGitInfo.owner !== owner && apiGitInfo.owner) + setOwner(apiGitInfo.owner); + if (apiGitInfo.repo !== repo && apiGitInfo.repo) setRepo(apiGitInfo.repo); + }, [apiGitInfo]); + return ( @@ -34,9 +40,9 @@ export const ProjectInput:FC<{ color="primary" onClick={() => setGitInfo({ owner, repo })} > - Load + Save ); -}; \ No newline at end of file +}; diff --git a/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx index 4c377f090c..8a0ba78118 100644 --- a/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx @@ -41,8 +41,8 @@ export const SettingsPage = () => { A description of your plugin goes here. - - + + {authed ? ( @@ -79,7 +79,14 @@ export const SettingsPage = () => { )} - api.setVCSOptions(info)} /> + + + + api.setVCSOptions(info)} + /> + From 7bda356234cc4d7dd8fbc271a77dffa47fd45a6f Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 4 May 2020 23:11:59 +0200 Subject: [PATCH 14/22] feat: start:backend package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 03c9b411fe..beddf9cab8 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ }, "scripts": { "start": "yarn workspace example-app start", + "start:backend": "yarn workspace example-backend start", "bundle": "yarn build && yarn workspace example-app bundle", "build": "lerna run build", "clean": "lerna run clean", From 9e7bc707280740d5c78aefcb10b9685bafc68fa6 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 5 May 2020 11:23:37 +0200 Subject: [PATCH 15/22] Restructure pages --- .../src/pages/BuildsPage/BuildsPage.tsx | 33 ++++++++++++++++++ .../circleci/src/pages/BuildsPage/index.ts | 1 + .../CircleCIPage/CircleCIPage.tsx | 34 +++---------------- .../CircleCIPage/index.ts | 0 .../SettingsPage/SettingsPage.tsx | 8 +---- .../SettingsPage/index.ts | 0 plugins/circleci/src/plugin.ts | 3 +- 7 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx create mode 100644 plugins/circleci/src/pages/BuildsPage/index.ts rename plugins/circleci/src/{components => pages}/CircleCIPage/CircleCIPage.tsx (54%) rename plugins/circleci/src/{components => pages}/CircleCIPage/index.ts (100%) rename plugins/circleci/src/{components => pages}/SettingsPage/SettingsPage.tsx (91%) rename plugins/circleci/src/{components => pages}/SettingsPage/index.ts (100%) diff --git a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx new file mode 100644 index 0000000000..cb8b3500ef --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx @@ -0,0 +1,33 @@ +import React, { FC } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { + Content, + ContentHeader, + SupportButton, + InfoCard, +} from '@backstage/core'; +import { Button, Grid } from '@material-ui/core'; +import { CircleCIFetch } from 'components/CircleCIFetch'; +import { Settings as SettingsIcon } from '@material-ui/icons'; + +export const BuildsPage: FC<{}> = () => ( + + + + A description of your plugin goes here. + + + + + + + + + +); diff --git a/plugins/circleci/src/pages/BuildsPage/index.ts b/plugins/circleci/src/pages/BuildsPage/index.ts new file mode 100644 index 0000000000..94086cd569 --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/index.ts @@ -0,0 +1 @@ +export * from './BuildsPage'; diff --git a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx b/plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx similarity index 54% rename from plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx rename to plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx index 32812b1ade..e648c20b3a 100644 --- a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx +++ b/plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx @@ -16,51 +16,25 @@ import React, { FC } from 'react'; import { Route } from 'react-router'; -import { Link as RouterLink } from 'react-router-dom'; -import { Grid, Button } from '@material-ui/core'; -import { Settings as SettingsIcon } from '@material-ui/icons'; import { - InfoCard, Header, Page, pageTheme, - Content, - ContentHeader, HeaderLabel, - SupportButton, } from '@backstage/core'; -import { CircleCIFetch } from '../CircleCIFetch'; import { SettingsPage } from '../SettingsPage'; +import { BuildsPage } from '../BuildsPage/BuildsPage'; export const CircleCIPage: FC<{}> = () => { return ( <> -
- - - - - A description of your plugin goes here. - - - - - - - - - - + + +
); diff --git a/plugins/circleci/src/components/CircleCIPage/index.ts b/plugins/circleci/src/pages/CircleCIPage/index.ts similarity index 100% rename from plugins/circleci/src/components/CircleCIPage/index.ts rename to plugins/circleci/src/pages/CircleCIPage/index.ts diff --git a/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx similarity index 91% rename from plugins/circleci/src/components/SettingsPage/SettingsPage.tsx rename to plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx index 8a0ba78118..95e2c86662 100644 --- a/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx @@ -4,12 +4,10 @@ import { circleCIApiRef } from 'api'; import { InfoCard, useApi, - Header, Page, pageTheme, Content, ContentHeader, - HeaderLabel, SupportButton, } from '@backstage/core'; import { ProjectInput } from 'components/ProjectInput/ProjectInput'; @@ -30,10 +28,6 @@ export const SettingsPage = () => { return ( -
- - -
A description of your plugin goes here. - + diff --git a/plugins/circleci/src/components/SettingsPage/index.ts b/plugins/circleci/src/pages/SettingsPage/index.ts similarity index 100% rename from plugins/circleci/src/components/SettingsPage/index.ts rename to plugins/circleci/src/pages/SettingsPage/index.ts diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 05d3710d40..c05feb2059 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -14,8 +14,7 @@ * limitations under the License. */ import { createPlugin } from '@backstage/core'; -import { CircleCIPage } from './components/CircleCIPage'; - +import { CircleCIPage } from './pages/CircleCIPage'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { From 20ef06f6f30d9c3e549e497ffe08b2eb41a2cfa8 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 5 May 2020 12:46:43 +0200 Subject: [PATCH 16/22] Add detailed view page with a list of build steps --- plugins/circleci/src/api/index.ts | 4 ++ .../src/components/CITable/CITable.tsx | 4 +- .../src/pages/CircleCIPage/CircleCIPage.tsx | 7 +-- .../DetailedViewPage/DetailedViewPage.tsx | 51 +++++++++++++++++++ 4 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 955daf3fba..362657326e 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -104,4 +104,8 @@ export class CircleCIApi { async getUser() { return this.api.me(); } + + async getBuild(buildId: string) { + return this.api.build(parseInt(buildId, 10)); + } } diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/components/CITable/CITable.tsx index 5efb0edb9f..465fae24e0 100644 --- a/plugins/circleci/src/components/CITable/CITable.tsx +++ b/plugins/circleci/src/components/CITable/CITable.tsx @@ -9,10 +9,10 @@ import { TableHead, TableContainer, TableRow, - Link, CircularProgress, } from '@material-ui/core'; import { Replay as RetryIcon } from '@material-ui/icons'; +import { Link } from 'react-router-dom'; import { StatusFailed, StatusOK, @@ -94,7 +94,7 @@ export const CITable: FC<{ {build.id} - + {build.buildName} diff --git a/plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx b/plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx index e648c20b3a..fe0ef0f1ce 100644 --- a/plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx +++ b/plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx @@ -23,7 +23,8 @@ import { HeaderLabel, } from '@backstage/core'; import { SettingsPage } from '../SettingsPage'; -import { BuildsPage } from '../BuildsPage/BuildsPage'; +import { BuildsPage } from '../BuildsPage'; +import { DetailedViewPage } from '../DetailedViewPage'; export const CircleCIPage: FC<{}> = () => { return ( <> @@ -33,8 +34,8 @@ export const CircleCIPage: FC<{}> = () => { - - + +
); diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx new file mode 100644 index 0000000000..e7441b979a --- /dev/null +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -0,0 +1,51 @@ +import React, { FC } from 'react'; +import { Content, InfoCard, useApi } from '@backstage/core'; +import { Grid, List, ListItem } from '@material-ui/core'; +import { PluginHeader } from 'components/PluginHeader'; +import { BuildWithSteps, BuildStep } from 'circleci-api'; +import { circleCIApiRef } from 'api'; +// import { LazyLog } from 'react-lazylog'; +import { useParams } from 'react-router-dom'; + +export const DetailedViewPage: FC<{}> = () => { + let { buildId = '' } = useParams(); + + console.log(useParams()); + + const [authed, setAuthed] = React.useState(false); + + //@ts-ignore + const [build, setBuild] = React.useState({}); + const api = useApi(circleCIApiRef); + + React.useEffect(() => { + const getBuildAsync = async () => { + if (!authed) { + await api.restorePersistedSettings(); + await api + .validateToken() + .then(() => { + setAuthed(true); + }) + .catch(() => setAuthed(false)); + } + api.getBuild(buildId).then(setBuild); + }; + getBuildAsync(); + }, [authed, buildId]); + + return ( + + + + + + + {build.steps && build.steps.map(({name}: {name: string}) => ({name}))} + + + + + + ); +}; From 950830571e21fd0ae49e9816e6027e18ce0d0985 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 5 May 2020 16:44:39 +0200 Subject: [PATCH 17/22] Add ugly log visualization --- plugins/circleci/src/api/index.ts | 1 + .../components/ActionOutput/ActionOutput.tsx | 14 +++++ .../src/components/ActionOutput/index.ts | 1 + .../components/PluginHeader/PluginHeader.tsx | 18 +++++++ .../src/components/PluginHeader/index.ts | 1 + .../DetailedViewPage/DetailedViewPage.tsx | 54 +++++++++++++------ .../src/pages/DetailedViewPage/index.ts | 1 + 7 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 plugins/circleci/src/components/ActionOutput/ActionOutput.tsx create mode 100644 plugins/circleci/src/components/ActionOutput/index.ts create mode 100644 plugins/circleci/src/components/PluginHeader/PluginHeader.tsx create mode 100644 plugins/circleci/src/components/PluginHeader/index.ts create mode 100644 plugins/circleci/src/pages/DetailedViewPage/index.ts diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 362657326e..7ce157057b 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -31,6 +31,7 @@ export const circleCIApiRef = new ApiRef({ }); export class CircleCIApi { + private token: string = ''; options: Partial; diff --git a/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx new file mode 100644 index 0000000000..79f8954a73 --- /dev/null +++ b/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx @@ -0,0 +1,14 @@ +import React, { useEffect, useState, FC } from 'react'; + +export const ActionOutput: FC<{ url: string }> = ({ url }) => { + //@ts-ignore + const [messages, setMessages] = useState([]); + useEffect(() => { + fetch(url) + .then(res => res.json()) + .then((messages) => { + messages && setMessages(messages.map(({message}: {message: string}) => message)) + }); + }, [url]); + return
{messages}
; +}; diff --git a/plugins/circleci/src/components/ActionOutput/index.ts b/plugins/circleci/src/components/ActionOutput/index.ts new file mode 100644 index 0000000000..1e4fd5dfac --- /dev/null +++ b/plugins/circleci/src/components/ActionOutput/index.ts @@ -0,0 +1 @@ +export { ActionOutput } from './ActionOutput'; diff --git a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx new file mode 100644 index 0000000000..f725a979a9 --- /dev/null +++ b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { ContentHeader, SupportButton } from '@backstage/core'; +import { Button } from '@material-ui/core'; +import { Settings as SettingsIcon } from '@material-ui/icons'; + +export const PluginHeader = () => ( + + + A description of your plugin goes here. + +); diff --git a/plugins/circleci/src/components/PluginHeader/index.ts b/plugins/circleci/src/components/PluginHeader/index.ts new file mode 100644 index 0000000000..e9231f1318 --- /dev/null +++ b/plugins/circleci/src/components/PluginHeader/index.ts @@ -0,0 +1 @@ +export * from './PluginHeader'; diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx index e7441b979a..ce1b87b36a 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -2,20 +2,16 @@ import React, { FC } from 'react'; import { Content, InfoCard, useApi } from '@backstage/core'; import { Grid, List, ListItem } from '@material-ui/core'; import { PluginHeader } from 'components/PluginHeader'; -import { BuildWithSteps, BuildStep } from 'circleci-api'; +import { BuildWithSteps, BuildStepAction } from 'circleci-api'; import { circleCIApiRef } from 'api'; -// import { LazyLog } from 'react-lazylog'; import { useParams } from 'react-router-dom'; +import { ActionOutput } from '../../components/ActionOutput/ActionOutput'; export const DetailedViewPage: FC<{}> = () => { let { buildId = '' } = useParams(); - console.log(useParams()); - const [authed, setAuthed] = React.useState(false); - - //@ts-ignore - const [build, setBuild] = React.useState({}); + const [build, setBuild] = React.useState(null); const api = useApi(circleCIApiRef); React.useEffect(() => { @@ -33,19 +29,45 @@ export const DetailedViewPage: FC<{}> = () => { }; getBuildAsync(); }, [authed, buildId]); - return ( - - - - - {build.steps && build.steps.map(({name}: {name: string}) => ({name}))} - - + {!api.authed ? ( +
Not authenticated
+ ) : ( + + + + + -
+ )}
); }; + +const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( + + {build && + build.steps && + build.steps.map( + ({ name, actions }: { name: string; actions: BuildStepAction[] }) => ( + + {name} +
+ +
+ ), + )} +
+); + +const ActionsList: FC<{ actions: BuildStepAction[] }> = ({ actions }) => ( + + {actions.map((action: BuildStepAction) => ( + + + + ))} + +); diff --git a/plugins/circleci/src/pages/DetailedViewPage/index.ts b/plugins/circleci/src/pages/DetailedViewPage/index.ts new file mode 100644 index 0000000000..578eec4e64 --- /dev/null +++ b/plugins/circleci/src/pages/DetailedViewPage/index.ts @@ -0,0 +1 @@ +export * from './DetailedViewPage'; From 8a9241619ef1516bbb4d58a75166b02310b00382 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 5 May 2020 22:40:43 +0200 Subject: [PATCH 18/22] Implement logs visualization --- .../components/ActionOutput/ActionOutput.tsx | 45 ++++++++++++++++--- .../DetailedViewPage/DetailedViewPage.tsx | 25 +++++------ 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx index 79f8954a73..b8d1564040 100644 --- a/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx +++ b/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx @@ -1,14 +1,47 @@ import React, { useEffect, useState, FC } from 'react'; +import { + ExpansionPanel, + ExpansionPanelSummary, + Typography, + ExpansionPanelDetails, +} from '@material-ui/core'; -export const ActionOutput: FC<{ url: string }> = ({ url }) => { - //@ts-ignore +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { BuildStepAction } from 'circleci-api'; + +export const ActionOutput: FC<{ + url: string; + name: string; + action: BuildStepAction; +}> = ({ url, name }) => { const [messages, setMessages] = useState([]); useEffect(() => { fetch(url) .then(res => res.json()) - .then((messages) => { - messages && setMessages(messages.map(({message}: {message: string}) => message)) - }); + .then(actionOutput => { + actionOutput && + setMessages( + actionOutput.map(({ message }: { message: string }) => message), + ); + }); }, [url]); - return
{messages}
; + console.log(messages); + return ( + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + {name} + + + {messages.length === 0 + ? 'Nothing here...' + : messages.map(message => ( +

{message}

+ ))} +
+
+ ); }; diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx index ce1b87b36a..53fe2fea3e 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; import { Content, InfoCard, useApi } from '@backstage/core'; -import { Grid, List, ListItem } from '@material-ui/core'; +import { Grid, Box } from '@material-ui/core'; import { PluginHeader } from 'components/PluginHeader'; import { BuildWithSteps, BuildStepAction } from 'circleci-api'; import { circleCIApiRef } from 'api'; @@ -37,8 +37,9 @@ export const DetailedViewPage: FC<{}> = () => { ) : ( - + + )} @@ -47,27 +48,21 @@ export const DetailedViewPage: FC<{}> = () => { }; const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( - + {build && build.steps && build.steps.map( ({ name, actions }: { name: string; actions: BuildStepAction[] }) => ( - - {name} -
- -
+ ), )} -
+ ); -const ActionsList: FC<{ actions: BuildStepAction[] }> = ({ actions }) => ( - +const ActionsList: FC<{ actions: BuildStepAction[], name: string }> = ({ actions, name }) => ( + {actions.map((action: BuildStepAction) => ( - - - + ))} - + ); From 633f592f81151e47dd3568e5c01dae7632ee9285 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 6 May 2020 10:31:49 +0200 Subject: [PATCH 19/22] feat: move proxy to devserver --- package.json | 1 - packages/app/package.json | 3 +-- packages/app/src/setupProxy.js | 7 +++++++ packages/backend/package.json | 1 - packages/backend/src/index.ts | 2 -- plugins/circleci-backend/.eslintrc.js | 3 --- plugins/circleci-backend/README.md | 6 ------ plugins/circleci-backend/package.json | 23 ---------------------- plugins/circleci-backend/src/index.ts | 27 -------------------------- plugins/circleci-backend/tsconfig.json | 15 -------------- plugins/circleci/src/api/index.ts | 2 +- plugins/circleci/src/index.ts | 1 + plugins/circleci/src/plugin.ts | 4 +++- plugins/circleci/src/proxy.ts | 12 ++++++++++++ yarn.lock | 7 ------- 15 files changed, 25 insertions(+), 89 deletions(-) create mode 100644 packages/app/src/setupProxy.js delete mode 100644 plugins/circleci-backend/.eslintrc.js delete mode 100644 plugins/circleci-backend/README.md delete mode 100644 plugins/circleci-backend/package.json delete mode 100644 plugins/circleci-backend/src/index.ts delete mode 100644 plugins/circleci-backend/tsconfig.json create mode 100644 plugins/circleci/src/proxy.ts diff --git a/package.json b/package.json index beddf9cab8..03c9b411fe 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ }, "scripts": { "start": "yarn workspace example-app start", - "start:backend": "yarn workspace example-backend start", "bundle": "yarn build && yarn workspace example-app bundle", "build": "lerna run build", "clean": "lerna run clean", diff --git a/packages/app/package.json b/packages/app/package.json index e88bd46a54..8e75937fd8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -4,7 +4,6 @@ "private": true, "devDependencies": { "@testing-library/cypress": "^6.0.0", - "@types/jquery": "^3.3.34", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", @@ -15,13 +14,13 @@ "cross-env": "^7.0.0", "cypress": "^4.2.0", "eslint-plugin-cypress": "^2.10.3", + "http-proxy-middleware": "^1.0.3", "start-server-and-test": "^1.10.11" }, "dependencies": { "@backstage/cli": "^0.1.1-alpha.4", "@backstage/core": "^0.1.1-alpha.4", "@backstage/plugin-circleci": "^0.1.1-alpha.4", - "@backstage/plugin-circleci-backend": "^0.1.1-alpha.4", "@backstage/plugin-home-page": "^0.1.1-alpha.4", "@backstage/plugin-inventory": "^0.1.1-alpha.4", "@backstage/plugin-lighthouse": "^0.1.1-alpha.4", diff --git a/packages/app/src/setupProxy.js b/packages/app/src/setupProxy.js new file mode 100644 index 0000000000..65ed973923 --- /dev/null +++ b/packages/app/src/setupProxy.js @@ -0,0 +1,7 @@ +const { proxySettings } = require('@backstage/plugin-circleci'); +const { createProxyMiddleware } = require('http-proxy-middleware'); + +module.exports = (/** @type {import('express').Application} */ app) => + Object.entries(proxySettings).forEach(([url, settings]) => + app.use(url, createProxyMiddleware(settings)), + ); diff --git a/packages/backend/package.json b/packages/backend/package.json index 13d64a0a13..0530f04452 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -16,7 +16,6 @@ }, "dependencies": { "@backstage/plugin-inventory-backend": "0.1.1-alpha.4", - "@backstage/plugin-circleci-backend": "0.1.1-alpha.4", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index da1a4f3dc3..fd8015c328 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -28,7 +28,6 @@ import helmet from 'helmet'; import compression from 'compression'; import { testRouter } from './test'; import { router as inventoryRouter } from '@backstage/plugin-inventory-backend'; -import { router as circleCIRouter } from '@backstage/plugin-circleci-backend'; const DEFAULT_PORT = 7000; @@ -41,7 +40,6 @@ app.use(compression()); app.use(express.json()); app.use('/test', testRouter); app.use('/inventory', inventoryRouter); -app.use('/circleci', circleCIRouter); app.listen(PORT, () => { console.log(`Listening on port ${PORT}`); diff --git a/plugins/circleci-backend/.eslintrc.js b/plugins/circleci-backend/.eslintrc.js deleted file mode 100644 index 13573efa9c..0000000000 --- a/plugins/circleci-backend/.eslintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; diff --git a/plugins/circleci-backend/README.md b/plugins/circleci-backend/README.md deleted file mode 100644 index 91e60e3e6b..0000000000 --- a/plugins/circleci-backend/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Title -Welcome to the circleci-backend plugin! - -## Sub-section 1 - -## Sub-section 2 diff --git a/plugins/circleci-backend/package.json b/plugins/circleci-backend/package.json deleted file mode 100644 index 6535deba10..0000000000 --- a/plugins/circleci-backend/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@backstage/plugin-circleci-backend", - "version": "0.1.1-alpha.4", - "main": "dist", - "license": "Apache-2.0", - "private": true, - "scripts": { - "build": "tsc", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "clean": "backstage-cli clean" - }, - "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.4" - }, - "dependencies": { - "express": "^4.17.1", - "http-proxy-middleware": "^1.0.3" - }, - "files": [ - "dist" - ] -} diff --git a/plugins/circleci-backend/src/index.ts b/plugins/circleci-backend/src/index.ts deleted file mode 100644 index 45bf47b884..0000000000 --- a/plugins/circleci-backend/src/index.ts +++ /dev/null @@ -1,27 +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 express from 'express'; -import httpProxy from 'http-proxy'; - -// Simple proxy for handling CORS for now -const proxy = httpProxy.createServer({ - target: 'https://circleci.com/api/v1.1', - changeOrigin: true, -}); -proxy.on('error', e => console.error(e)); -export const router = express(); -router.use('/api', (req, res) => proxy.web(req, res)); diff --git a/plugins/circleci-backend/tsconfig.json b/plugins/circleci-backend/tsconfig.json deleted file mode 100644 index b463ac102f..0000000000 --- a/plugins/circleci-backend/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "include": ["src"], - "compilerOptions": { - "baseUrl": "src", - "outDir": "dist", - "incremental": true, - "sourceMap": true, - "declaration": true, - "strict": true, - "target": "es5", - "module": "commonjs", - "esModuleInterop": true, - "types": ["node", "jest"] - } -} diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 955daf3fba..afbd74f28f 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -18,7 +18,7 @@ import { CircleCI, GitType, CircleCIOptions } from 'circleci-api'; import { ApiRef } from '@backstage/core'; const defaultOptions: Partial = { - circleHost: 'http://backstage.localhost:7000/circleci/api', + circleHost: '/circleci/api', vcs: { type: GitType.GITHUB, owner: 'CircleCITest3', diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts index d67bc6a864..33b8fe0067 100644 --- a/plugins/circleci/src/index.ts +++ b/plugins/circleci/src/index.ts @@ -16,3 +16,4 @@ export { plugin } from './plugin'; export * from './api'; +export * from './proxy'; diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 05d3710d40..458a7f27bc 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -15,10 +15,12 @@ */ import { createPlugin } from '@backstage/core'; import { CircleCIPage } from './components/CircleCIPage'; +import { SettingsPage } from './components/SettingsPage'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { - router.registerRoute('/circleci', CircleCIPage, { exact: false }); + router.registerRoute('/circleci', CircleCIPage); + router.registerRoute('/circleci/settings', SettingsPage); }, }); diff --git a/plugins/circleci/src/proxy.ts b/plugins/circleci/src/proxy.ts new file mode 100644 index 0000000000..397b9b3c56 --- /dev/null +++ b/plugins/circleci/src/proxy.ts @@ -0,0 +1,12 @@ +import type {Options} from 'http-proxy-middleware'; + +export const proxySettings: Record = { + '/circleci/api': { + target: 'https://circleci.com/api/v1.1', + changeOrigin: true, + logLevel: 'debug', + pathRewrite: { + '^/circleci/api/': '/', + }, + }, +}; diff --git a/yarn.lock b/yarn.lock index b3b8467ce5..d337b98883 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4072,13 +4072,6 @@ dependencies: "@types/sizzle" "*" -"@types/jquery@^3.3.34": - version "3.3.34" - resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.34.tgz#0d3b94057063d3854adaeb579652048fec07ba6c" - integrity sha512-lW9vsVL53Xu/Nj4gi2hNmHGc4u3KKghjqTkAlO0kF5GIOPxbqqnQpgqJBzmn3yXLrPqHb6cmNJ6URnS23Vtvbg== - dependencies: - "@types/sizzle" "*" - "@types/js-cookie@2.2.5": version "2.2.5" resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e" From 6b44dd110c5ca3b009f15b4f8e6cac6c95f061b2 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 6 May 2020 11:18:49 +0200 Subject: [PATCH 20/22] feat: routing -> plugin routing --- .../circleci/src/components/Layout/Layout.tsx | 12 +++++ .../circleci/src/components/Layout/index.ts | 1 + .../src/pages/BuildsPage/BuildsPage.tsx | 39 ++++++++-------- .../src/pages/CircleCIPage/CircleCIPage.tsx | 44 ------------------- .../circleci/src/pages/CircleCIPage/index.ts | 1 - .../DetailedViewPage/DetailedViewPage.tsx | 42 +++++++++++------- .../src/pages/SettingsPage/SettingsPage.tsx | 9 ++-- plugins/circleci/src/plugin.ts | 6 ++- 8 files changed, 68 insertions(+), 86 deletions(-) create mode 100644 plugins/circleci/src/components/Layout/Layout.tsx create mode 100644 plugins/circleci/src/components/Layout/index.ts delete mode 100644 plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx delete mode 100644 plugins/circleci/src/pages/CircleCIPage/index.ts diff --git a/plugins/circleci/src/components/Layout/Layout.tsx b/plugins/circleci/src/components/Layout/Layout.tsx new file mode 100644 index 0000000000..fe82bf90e8 --- /dev/null +++ b/plugins/circleci/src/components/Layout/Layout.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core'; + +export const Layout: React.FC = ({ children }) => ( + +
+ + +
+ {children} +
+); diff --git a/plugins/circleci/src/components/Layout/index.ts b/plugins/circleci/src/components/Layout/index.ts new file mode 100644 index 0000000000..9877e7f4ae --- /dev/null +++ b/plugins/circleci/src/components/Layout/index.ts @@ -0,0 +1 @@ +export * from './Layout'; diff --git a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx index cb8b3500ef..98e634d4c0 100644 --- a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx +++ b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx @@ -9,25 +9,28 @@ import { import { Button, Grid } from '@material-ui/core'; import { CircleCIFetch } from 'components/CircleCIFetch'; import { Settings as SettingsIcon } from '@material-ui/icons'; +import { Layout } from 'components/Layout'; export const BuildsPage: FC<{}> = () => ( - - - - A description of your plugin goes here. - - - - - - + + + + + A description of your plugin goes here. + + + + + + + - - + + ); diff --git a/plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx b/plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx deleted file mode 100644 index fe0ef0f1ce..0000000000 --- a/plugins/circleci/src/pages/CircleCIPage/CircleCIPage.tsx +++ /dev/null @@ -1,44 +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 React, { FC } from 'react'; -import { Route } from 'react-router'; -import { - Header, - Page, - pageTheme, - HeaderLabel, -} from '@backstage/core'; -import { SettingsPage } from '../SettingsPage'; -import { BuildsPage } from '../BuildsPage'; -import { DetailedViewPage } from '../DetailedViewPage'; -export const CircleCIPage: FC<{}> = () => { - return ( - <> - -
- - -
- - - -
- - ); -}; - -export default CircleCIPage; diff --git a/plugins/circleci/src/pages/CircleCIPage/index.ts b/plugins/circleci/src/pages/CircleCIPage/index.ts deleted file mode 100644 index 188ed6d61f..0000000000 --- a/plugins/circleci/src/pages/CircleCIPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CircleCIPage } from './CircleCIPage'; diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx index 53fe2fea3e..3d5d7b575b 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -6,6 +6,7 @@ import { BuildWithSteps, BuildStepAction } from 'circleci-api'; import { circleCIApiRef } from 'api'; import { useParams } from 'react-router-dom'; import { ActionOutput } from '../../components/ActionOutput/ActionOutput'; +import { Layout } from 'components/Layout'; export const DetailedViewPage: FC<{}> = () => { let { buildId = '' } = useParams(); @@ -30,20 +31,22 @@ export const DetailedViewPage: FC<{}> = () => { getBuildAsync(); }, [authed, buildId]); return ( - - - {!api.authed ? ( -
Not authenticated
- ) : ( - - - - - + + + + {!api.authed ? ( +
Not authenticated
+ ) : ( + + + + + + -
- )} -
+ )} + + ); }; @@ -53,16 +56,23 @@ const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( build.steps && build.steps.map( ({ name, actions }: { name: string; actions: BuildStepAction[] }) => ( - + ), )} ); -const ActionsList: FC<{ actions: BuildStepAction[], name: string }> = ({ actions, name }) => ( +const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({ + actions, + name, +}) => ( {actions.map((action: BuildStepAction) => ( - + ))} ); diff --git a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx index 95e2c86662..af380509d1 100644 --- a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx @@ -4,14 +4,13 @@ import { circleCIApiRef } from 'api'; import { InfoCard, useApi, - Page, - pageTheme, Content, ContentHeader, SupportButton, } from '@backstage/core'; import { ProjectInput } from 'components/ProjectInput/ProjectInput'; import { Link as RouterLink } from 'react-router-dom'; +import { Layout } from 'components/Layout'; export const SettingsPage = () => { const api = useApi(circleCIApiRef); @@ -27,7 +26,7 @@ export const SettingsPage = () => { }, []); return ( - + A description of your plugin goes here. - + @@ -84,6 +83,6 @@ export const SettingsPage = () => { - + ); }; diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 7e96bf5e0d..11d215f30f 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -14,13 +14,15 @@ * limitations under the License. */ import { createPlugin } from '@backstage/core'; -import { CircleCIPage } from './pages/CircleCIPage'; +import { BuildsPage } from './pages/BuildsPage'; import { SettingsPage } from './pages/SettingsPage'; +import { DetailedViewPage } from './pages/DetailedViewPage'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { - router.registerRoute('/circleci', CircleCIPage); + router.registerRoute('/circleci', BuildsPage); + router.registerRoute('/circleci/build/:buildId', DetailedViewPage); router.registerRoute('/circleci/settings', SettingsPage); }, }); From 684408f0ffc01cb85be8f67c3b129f5a17b3cd3c Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 6 May 2020 12:37:26 +0200 Subject: [PATCH 21/22] feat: lazylog --- plugins/circleci/package.json | 2 + .../components/ActionOutput/ActionOutput.tsx | 25 +++-- .../DetailedViewPage/DetailedViewPage.tsx | 5 +- plugins/circleci/tsconfig.json | 1 + yarn.lock | 91 ++++++++++++++++++- 5 files changed, 109 insertions(+), 15 deletions(-) diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 73b10d4fa5..cf5c5f93c4 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -27,9 +27,11 @@ "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "@types/react-lazylog": "^4.5.0", "circleci-api": "^4.0.0", "react": "16.13.1", "react-dom": "16.13.1", + "react-lazylog": "^4.5.2", "react-router": "^5.1.2", "react-use": "^13.0.0" }, diff --git a/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx index b8d1564040..7cc92098a1 100644 --- a/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx +++ b/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, FC } from 'react'; +import React, { useEffect, useState, FC, Suspense } from 'react'; import { ExpansionPanel, ExpansionPanelSummary, @@ -9,6 +9,8 @@ import { import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { BuildStepAction } from 'circleci-api'; +const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); + export const ActionOutput: FC<{ url: string; name: string; @@ -25,22 +27,25 @@ export const ActionOutput: FC<{ ); }); }, [url]); - console.log(messages); return ( - + } - aria-controls="panel1a-content" - id="panel1a-header" + aria-controls={`panel-${name}-content`} + id={`panel-${name}-header`} > {name} - {messages.length === 0 - ? 'Nothing here...' - : messages.map(message => ( -

{message}

- ))} + {messages.length === 0 ? ( + 'Nothing here...' + ) : ( + +
+ +
+
+ )}
); diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx index 3d5d7b575b..37ff1e0655 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -64,9 +64,8 @@ const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({ actions, - name, }) => ( - + <> {actions.map((action: BuildStepAction) => ( = ({ url={action.output_url || ''} /> ))} - + ); diff --git a/plugins/circleci/tsconfig.json b/plugins/circleci/tsconfig.json index 7b73db2f0f..91968a6e49 100644 --- a/plugins/circleci/tsconfig.json +++ b/plugins/circleci/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.json", "include": ["src"], "compilerOptions": { + "module": "esnext", "baseUrl": "src" } } diff --git a/yarn.lock b/yarn.lock index d337b98883..a55b15593a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2491,6 +2491,13 @@ prop-types "^15.7.2" react-is "^16.8.0" +"@mattiasbuelens/web-streams-polyfill@^0.2.0": + version "0.2.1" + resolved "https://registry.npmjs.org/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.2.1.tgz#d7c4aa94f98084ec0787be084d47167d62ea5f67" + integrity sha512-oKuFCQFa3W7Hj7zKn0+4ypI8JFm4ZKIoncwAC6wd5WwFW2sL7O1hpPoJdSWpynQ4DJ4lQ6MvFoVDmCLilonDFg== + dependencies: + "@types/whatwg-streams" "^0.0.7" + "@mdx-js/react@^1.0.0", "@mdx-js/react@^1.5.2": version "1.5.9" resolved "https://registry.npmjs.org/@mdx-js/react/-/react-1.5.9.tgz#31873ab097fbe58c61c7585fc0be64e83182b6df" @@ -4199,6 +4206,14 @@ dependencies: "@types/react" "*" +"@types/react-lazylog@^4.5.0": + version "4.5.0" + resolved "https://registry.npmjs.org/@types/react-lazylog/-/react-lazylog-4.5.0.tgz#1677ac520f29ada2e95b8cd5f8ec54b897227278" + integrity sha512-yg0IhKtDRGqUUtXJwi9Y/sSmgX/gmVP0vu0cr5uAECmJnRQbAgsGoVUntQGtwH0Z7Y2iiTh+QW5LkOFsmtIisg== + dependencies: + "@types/react" "*" + immutable ">=3.8.2" + "@types/react-router-dom@^5.1.3": version "5.1.3" resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196" @@ -4443,6 +4458,11 @@ "@types/webpack-sources" "*" source-map "^0.6.0" +"@types/whatwg-streams@^0.0.7": + version "0.0.7" + resolved "https://registry.npmjs.org/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz#28bfe73dc850562296367249c4b32a50db81e9d3" + integrity sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A== + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -6638,7 +6658,7 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= -clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: +clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702" integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA== @@ -8135,6 +8155,14 @@ dom-converter@^0.2: dependencies: utila "~0.4" +dom-helpers@^5.0.0: + version "5.1.4" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" + integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^2.6.7" + dom-helpers@^5.0.1: version "5.1.3" resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" @@ -9286,6 +9314,11 @@ feature-policy@0.3.0: resolved "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069" integrity sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ== +fetch-readablestream@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795" + integrity sha512-qu4mXWf4wus4idBIN/kVH+XSer8IZ9CwHP+Pd7DL7TuKNC1hP7ykon4kkBjwJF3EMX2WsFp4hH7gU7CyL7ucXw== + figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -10913,6 +10946,11 @@ immer@1.10.0: resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immutable@>=3.8.2, immutable@^3.8.2: + version "3.8.2" + resolved "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" + integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM= + import-cwd@^2.0.0, import-cwd@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -13610,7 +13648,7 @@ longest@^2.0.1: resolved "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8" integrity sha1-eB4YMpaqlPbU2RbcM10NF676I/g= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -14198,6 +14236,11 @@ mississippi@^3.0.0: stream-each "^1.1.0" through2 "^2.0.0" +mitt@^1.1.2: + version "1.2.0" + resolved "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" + integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -17114,6 +17157,21 @@ react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-i resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-lazylog@^4.5.2: + version "4.5.2" + resolved "https://registry.npmjs.org/react-lazylog/-/react-lazylog-4.5.2.tgz#9b66a0997348690f56286f6afcda97ebd0c62b7c" + integrity sha512-XvAjlzs8tzbjmqyEdj8HwW8Kg5nfqZAzIGeeG1incZuhuXQkekIs6nYb3W/GQNxDpA1CowgNUR4UfP+7/C2Ang== + dependencies: + "@mattiasbuelens/web-streams-polyfill" "^0.2.0" + fetch-readablestream "^0.2.0" + immutable "^3.8.2" + mitt "^1.1.2" + prop-types "^15.6.1" + react-string-replace "^0.4.1" + react-virtualized "^9.21.0" + text-encoding-utf-8 "^1.0.1" + whatwg-fetch "^2.0.4" + react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -17267,6 +17325,13 @@ react-sparklines@^1.7.0: dependencies: prop-types "^15.5.10" +react-string-replace@^0.4.1: + version "0.4.4" + resolved "https://registry.npmjs.org/react-string-replace/-/react-string-replace-0.4.4.tgz#24006fbe0db573d5be583133df38b1a735cb4225" + integrity sha512-FAMkhxmDpCsGTwTZg7p/2v+/GTmxAp73so3fbSvlAcBBX36ujiGRNEaM/1u+jiYQrArhns+7eE92g2pi5E5FUA== + dependencies: + lodash "^4.17.4" + react-syntax-highlighter@^11.0.2: version "11.0.2" resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-11.0.2.tgz#4e3f376e752b20d2f54e4c55652fd663149e4029" @@ -17334,6 +17399,18 @@ react-use@^13.24.0: ts-easing "^0.2.0" tslib "^1.10.0" +react-virtualized@^9.21.0: + version "9.21.2" + resolved "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" + integrity sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA== + dependencies: + babel-runtime "^6.26.0" + clsx "^1.0.1" + dom-helpers "^5.0.0" + loose-envify "^1.3.0" + prop-types "^15.6.0" + react-lifecycles-compat "^3.0.4" + react@16.13.1, react@^16.0.0, react@^16.12.0, react@^16.13.1, react@^16.8.3: version "16.13.1" resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" @@ -19651,6 +19728,11 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-encoding-utf-8@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" + integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== + text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" @@ -20788,6 +20870,11 @@ whatwg-fetch@3.0.0, whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0: resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== +whatwg-fetch@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" + integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== + whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" From ca7d863d065c25ce8fd7a8a0c86db6cc50d45c9a Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 6 May 2020 15:24:01 +0200 Subject: [PATCH 22/22] Extract CITable rows into components --- .../src/components/CITable/CITable.tsx | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/components/CITable/CITable.tsx index 465fae24e0..bd8be803a3 100644 --- a/plugins/circleci/src/components/CITable/CITable.tsx +++ b/plugins/circleci/src/components/CITable/CITable.tsx @@ -71,6 +71,48 @@ const getStatusComponent = (status: string) => { } }; +export const CITableBuildRow: FC<{ build: CITableBuildInfo }> = ({ build }) => ( + + {build.id} + + {build.buildName} + + + {build.source.branchName} +
+ {build.source.commit.hash} +
+ {getStatusComponent(build.status)} + {build.tests && ( + + { + <> + {build.tests.passed}/{build.tests.total} ( + {build.tests.failed ? build.tests.failed + ', ' : ''} + {build.tests.skipped ? build.tests.skipped : ''}) + + } + + )} + + + +
+); + +export const CITableBuildHeadRow:FC<{isTestDataAvailable: boolean}> = ({isTestDataAvailable}) => ( + + ID + Build + Source + Status + {isTestDataAvailable && Tests} + Actions + +); + export const CITable: FC<{ builds: CITableBuildInfo[]; }> = ({ builds }) => { @@ -79,50 +121,10 @@ export const CITable: FC<{ return (
- - - ID - Build - Source - Status - {isTestDataAvailable && Tests} - Actions - - + {builds.map(build => ( - - {build.id} - - - {build.buildName} - - - - {build.source.branchName} -
- {build.source.commit.hash} -
- - {getStatusComponent(build.status)} - - {build.tests && ( - - { - <> - {build.tests.passed}/{build.tests.total} ( - {build.tests.failed ? build.tests.failed + ', ' : ''} - {build.tests.skipped ? build.tests.skipped : ''}) - - } - - )} - - - -
+ ))}