From 462b644d2462e7e4b42184095cf455c36c1e5870 Mon Sep 17 00:00:00 2001 From: Matt Ray Date: Tue, 6 Jun 2023 15:30:09 +1000 Subject: [PATCH] Initial OpenCost plugin with light changes from `yarn new --select plugin` Signed-off-by: Matt Ray --- plugins/opencost/.eslintrc.js | 1 + plugins/opencost/README.md | 35 ++++++ plugins/opencost/dev/index.tsx | 27 +++++ plugins/opencost/package.json | 51 ++++++++ .../ExampleComponent.test.tsx | 42 +++++++ .../ExampleComponent/ExampleComponent.tsx | 54 +++++++++ .../src/components/ExampleComponent/index.ts | 16 +++ .../ExampleFetchComponent.test.tsx | 40 +++++++ .../ExampleFetchComponent.tsx | 111 ++++++++++++++++++ .../components/ExampleFetchComponent/index.ts | 16 +++ plugins/opencost/src/images/logo.png | Bin 0 -> 19329 bytes plugins/opencost/src/index.ts | 16 +++ plugins/opencost/src/plugin.test.ts | 22 ++++ plugins/opencost/src/plugin.ts | 37 ++++++ plugins/opencost/src/routes.ts | 20 ++++ plugins/opencost/src/setupTests.ts | 17 +++ 16 files changed, 505 insertions(+) create mode 100644 plugins/opencost/.eslintrc.js create mode 100644 plugins/opencost/README.md create mode 100644 plugins/opencost/dev/index.tsx create mode 100644 plugins/opencost/package.json create mode 100644 plugins/opencost/src/components/ExampleComponent/ExampleComponent.test.tsx create mode 100644 plugins/opencost/src/components/ExampleComponent/ExampleComponent.tsx create mode 100644 plugins/opencost/src/components/ExampleComponent/index.ts create mode 100644 plugins/opencost/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx create mode 100644 plugins/opencost/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx create mode 100644 plugins/opencost/src/components/ExampleFetchComponent/index.ts create mode 100644 plugins/opencost/src/images/logo.png create mode 100644 plugins/opencost/src/index.ts create mode 100644 plugins/opencost/src/plugin.test.ts create mode 100644 plugins/opencost/src/plugin.ts create mode 100644 plugins/opencost/src/routes.ts create mode 100644 plugins/opencost/src/setupTests.ts diff --git a/plugins/opencost/.eslintrc.js b/plugins/opencost/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/opencost/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/opencost/README.md b/plugins/opencost/README.md new file mode 100644 index 0000000000..d03ff4d624 --- /dev/null +++ b/plugins/opencost/README.md @@ -0,0 +1,35 @@ +# OpenCost + +Welcome to the OpenCost plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/opencost](http://localhost:3000/opencost). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. + +## IDEAS + +opencost logo on side + +### variables for: + + opencost URL + days of data (14d default) + costs warning lines + +top graph: +pick with specific query pattern +dates adjustable + +table underneath + +TrendLines + +Tabs on different namespaces/etc.? + +get currencies from cost-insights diff --git a/plugins/opencost/dev/index.tsx b/plugins/opencost/dev/index.tsx new file mode 100644 index 0000000000..41b2bd5346 --- /dev/null +++ b/plugins/opencost/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { openCostPlugin, OpenCostPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(openCostPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/opencost', + }) + .render(); diff --git a/plugins/opencost/package.json b/plugins/opencost/package.json new file mode 100644 index 0000000000..4c493136c0 --- /dev/null +++ b/plugins/opencost/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-opencost", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.60", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/opencost/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/opencost/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 0000000000..77c1dd610c --- /dev/null +++ b/plugins/opencost/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ExampleComponent } from './ExampleComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { screen } from '@testing-library/react'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from '@backstage/test-utils'; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + setupRequestMockHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + await renderInTestApp(); + expect(screen.getByText('Welcome to OpenCost!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/opencost/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/opencost/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 0000000000..fbeba9305a --- /dev/null +++ b/plugins/opencost/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core-components'; +import { ExampleFetchComponent } from '../ExampleFetchComponent'; + +export const ExampleComponent = () => ( + +
+ OpenCost + + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + + + + + + + +
+); diff --git a/plugins/opencost/src/components/ExampleComponent/index.ts b/plugins/opencost/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..7e520a7ab8 --- /dev/null +++ b/plugins/opencost/src/components/ExampleComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ExampleComponent } from './ExampleComponent'; diff --git a/plugins/opencost/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/opencost/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx new file mode 100644 index 0000000000..a714f640da --- /dev/null +++ b/plugins/opencost/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { ExampleFetchComponent } from './ExampleFetchComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; + +describe('ExampleFetchComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + setupRequestMockHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('https://randomuser.me/*', (_, res, ctx) => + res(ctx.status(200), ctx.delay(2000), ctx.json({})), + ), + ); + }); + it('should render', async () => { + await render(); + expect(await screen.findByTestId('progress')).toBeInTheDocument(); + }); +}); diff --git a/plugins/opencost/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/opencost/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx new file mode 100644 index 0000000000..c3403e1923 --- /dev/null +++ b/plugins/opencost/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -0,0 +1,111 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { + Table, + TableColumn, + Progress, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { fetchApiRef, useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; + +const useStyles = makeStyles({ + avatar: { + height: 32, + width: 32, + borderRadius: '50%', + }, +}); + +type User = { + gender: string; // "male" + name: { + title: string; // "Mr", + first: string; // "Duane", + last: string; // "Reed" + }; + location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…} + email: string; // "duane.reed@example.com" + login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…} + dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37} + registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14} + phone: string; // "07-2154-5651" + cell: string; // "0405-592-879" + id: { + name: string; // "TFN", + value: string; // "796260432" + }; + picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…} + nat: string; // "AU" +}; + +type DenseTableProps = { + users: User[]; +}; + +export const DenseTable = ({ users }: DenseTableProps) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { title: 'Avatar', field: 'avatar' }, + { title: 'Name', field: 'name' }, + { title: 'Email', field: 'email' }, + { title: 'Nationality', field: 'nationality' }, + ]; + + const data = users.map(user => { + return { + avatar: ( + {user.name.first} + ), + name: `${user.name.first} ${user.name.last}`, + email: user.email, + nationality: user.nat, + }; + }); + + return ( + + ); +}; + +export const ExampleFetchComponent = () => { + const { fetch } = useApi(fetchApiRef); + 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 ; + } + + return ; +}; diff --git a/plugins/opencost/src/components/ExampleFetchComponent/index.ts b/plugins/opencost/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 0000000000..9644c904ea --- /dev/null +++ b/plugins/opencost/src/components/ExampleFetchComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/plugins/opencost/src/images/logo.png b/plugins/opencost/src/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..34fdda4c61ef7d0c8629ba7244555ec999518829 GIT binary patch literal 19329 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 lXDm(nCZ { + it('should export plugin', () => { + expect(openCostPlugin).toBeDefined(); + }); +}); diff --git a/plugins/opencost/src/plugin.ts b/plugins/opencost/src/plugin.ts new file mode 100644 index 0000000000..6a97175770 --- /dev/null +++ b/plugins/opencost/src/plugin.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +export const openCostPlugin = createPlugin({ + id: 'opencost', + routes: { + root: rootRouteRef, + }, +}); + +export const OpenCostPage = openCostPlugin.provide( + createRoutableExtension({ + name: 'OpenCostPage', + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/opencost/src/routes.ts b/plugins/opencost/src/routes.ts new file mode 100644 index 0000000000..b7ace600e2 --- /dev/null +++ b/plugins/opencost/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'opencost', +}); diff --git a/plugins/opencost/src/setupTests.ts b/plugins/opencost/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/opencost/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill';