diff --git a/plugins/argocd/.eslintrc.js b/plugins/argocd/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/argocd/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/argocd/OWNERS b/plugins/argocd/OWNERS new file mode 100644 index 0000000000..9f4ccefff9 --- /dev/null +++ b/plugins/argocd/OWNERS @@ -0,0 +1,6 @@ +approvers: + - karthikjeeyar + - rohitkrai03 +reviewers: + - karthikjeeyar + - rohitkrai03 \ No newline at end of file diff --git a/plugins/argocd/README.md b/plugins/argocd/README.md new file mode 100644 index 0000000000..f62e1f4b92 --- /dev/null +++ b/plugins/argocd/README.md @@ -0,0 +1,54 @@ +# argocd + +Welcome to the argocd 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 [/argocd](http://localhost:3000/argocd). + +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. + +## Loading as Dynamic Plugin + +This plugin can be loaded in backstage showcase application as a dynamic plugin. + +Follow the below steps - + +- Export dynamic plugin assets. This will build and create the static assets for the plugin and put it inside dist-scalprum folder. + +```sh +yarn export-dynamic +``` + +- Package and copy dist-scalprum folder assets to dynamic-plugins-root folder in showcase application. + +```sh +pkg=../plugins/argocd +archive=$(npm pack $pkg) +tar -xzf "$archive" && rm "$archive" +mv package $(echo $archive | sed -e 's:\.tgz$::') +``` + +- Add the extension point inside the app-config.yaml or app-config.local.yaml file. + +```yaml +dynamicPlugins: + frontend: + janus-idp.backstage-plugin-argocd: + mountPoints: + - mountPoint: entity.page.cd/cards + importName: ArgocdPage + config: + layout: + gridColumn: '1 / -1' + if: + anyOf: + - hasAnnotation: backstage.io/kubernetes-id + - hasAnnotation: backstage.io/kubernetes-namespace +``` + +For more detailed explanation on dynamic plugins follow this [doc](https://github.com/janus-idp/backstage-showcase/blob/main/showcase-docs/dynamic-plugins.md). diff --git a/plugins/argocd/app-config.janus-idp.yaml b/plugins/argocd/app-config.janus-idp.yaml new file mode 100644 index 0000000000..a058ef78a3 --- /dev/null +++ b/plugins/argocd/app-config.janus-idp.yaml @@ -0,0 +1,13 @@ +dynamicPlugins: + frontend: + janus-idp.backstage-plugin-argocd: + mountPoints: + - mountPoint: entity.page.cd/cards + importName: ArgocdPage + config: + layout: + gridColumn: '1 / -1' + if: + anyOf: + - hasAnnotation: backstage.io/kubernetes-id + - hasAnnotation: backstage.io/kubernetes-namespace diff --git a/plugins/argocd/config.d.ts b/plugins/argocd/config.d.ts new file mode 100644 index 0000000000..5728ccd9e1 --- /dev/null +++ b/plugins/argocd/config.d.ts @@ -0,0 +1 @@ +export interface Config {} diff --git a/plugins/argocd/dev/index.tsx b/plugins/argocd/dev/index.tsx new file mode 100644 index 0000000000..e7d546aa8f --- /dev/null +++ b/plugins/argocd/dev/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { createDevApp } from '@backstage/dev-utils'; + +import { ArgocdPage, argocdPlugin } from '../src/plugin'; + +createDevApp() + .registerPlugin(argocdPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/argocd', + }) + .render(); diff --git a/plugins/argocd/package.json b/plugins/argocd/package.json new file mode 100644 index 0000000000..fafa818433 --- /dev/null +++ b/plugins/argocd/package.json @@ -0,0 +1,76 @@ +{ + "name": "@janus-idp/backstage-plugin-argocd", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "sideEffects": false, + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "export-dynamic": "janus-cli package export-dynamic-plugin", + "lint": "backstage-cli package lint", + "postpack": "backstage-cli package postpack", + "postversion": "yarn run export-dynamic", + "prepack": "backstage-cli package prepack", + "start": "backstage-cli package start", + "test": "backstage-cli package test --passWithNoTests --coverage", + "tsc": "tsc" + }, + "dependencies": { + "@backstage/core-components": "^0.14.0", + "@backstage/core-plugin-api": "^1.9.0", + "@backstage/theme": "^0.5.1", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.61", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "16.13.1 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "0.25.2", + "@backstage/core-app-api": "1.12.0", + "@backstage/dev-utils": "1.0.27", + "@backstage/test-utils": "1.5.0", + "@janus-idp/cli": "1.7.5", + "@testing-library/jest-dom": "6.0.0", + "@testing-library/react": "14.0.0", + "@testing-library/user-event": "14.0.0", + "msw": "1.0.0" + }, + "files": [ + "dist", + "config.d.ts", + "dist-scalprum", + "app-config.janus-idp.yaml" + ], + "scalprum": { + "name": "janus-idp.backstage-plugin-argocd", + "exposedModules": { + "PluginRoot": "./src/index.ts" + } + }, + "configSchema": "config.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/janus-idp/backstage-plugins", + "directory": "plugins/argocd" + }, + "keywords": [ + "backstage", + "plugin" + ], + "homepage": "https://janus-idp.io/", + "bugs": "https://github.com/janus-idp/backstage-plugins/issues" +} diff --git a/plugins/argocd/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/argocd/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 0000000000..3e8049baf3 --- /dev/null +++ b/plugins/argocd/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import { + renderInTestApp, + setupRequestMockHandlers, +} from '@backstage/test-utils'; + +import { screen } from '@testing-library/react'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +import { ExampleComponent } from './ExampleComponent'; + +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 ArgoCD!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/argocd/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/argocd/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 0000000000..a14435beec --- /dev/null +++ b/plugins/argocd/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { + Content, + ContentHeader, + Header, + HeaderLabel, + InfoCard, + Page, + SupportButton, +} from '@backstage/core-components'; + +import { Grid, Typography } from '@material-ui/core'; + +import { ExampleFetchComponent } from '../ExampleFetchComponent'; + +export const ExampleComponent = () => ( + +
+ + +
+ + + + This is the official ArgoCD plugin from Red Hat. + + + + + + + All content should be wrapped in a card like this. + + + + + + + + +
+); diff --git a/plugins/argocd/src/components/ExampleComponent/index.ts b/plugins/argocd/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..8b8437521b --- /dev/null +++ b/plugins/argocd/src/components/ExampleComponent/index.ts @@ -0,0 +1 @@ +export { ExampleComponent } from './ExampleComponent'; diff --git a/plugins/argocd/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/argocd/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx new file mode 100644 index 0000000000..6abcc6c5d0 --- /dev/null +++ b/plugins/argocd/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { render, screen } from '@testing-library/react'; + +import { ExampleFetchComponent } from './ExampleFetchComponent'; + +describe('ExampleFetchComponent', () => { + it('renders the user table', async () => { + render(); + + // Wait for the table to render + const table = await screen.findByRole('table'); + const nationality = screen.getAllByText('GB'); + // Assert that the table contains the expected user data + expect(table).toBeInTheDocument(); + expect(screen.getByAltText('Carolyn')).toBeInTheDocument(); + expect(screen.getByText('Carolyn Moore')).toBeInTheDocument(); + expect(screen.getByText('carolyn.moore@example.com')).toBeInTheDocument(); + expect(nationality[0]).toBeInTheDocument(); + }); +}); diff --git a/plugins/argocd/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/argocd/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx new file mode 100644 index 0000000000..ee2a35a5e8 --- /dev/null +++ b/plugins/argocd/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -0,0 +1,310 @@ +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; + +import { + Progress, + ResponseErrorPanel, + Table, + TableColumn, +} from '@backstage/core-components'; + +import { makeStyles } from '@material-ui/core/styles'; + +export const exampleUsers = { + results: [ + { + gender: 'female', + name: { + title: 'Miss', + first: 'Carolyn', + last: 'Moore', + }, + email: 'carolyn.moore@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Carolyn', + nat: 'GB', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Esma', + last: 'Berberoğlu', + }, + email: 'esma.berberoglu@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Esma', + nat: 'TR', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Isabella', + last: 'Rhodes', + }, + email: 'isabella.rhodes@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella', + nat: 'GB', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Derrick', + last: 'Carter', + }, + email: 'derrick.carter@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Derrick', + nat: 'IE', + }, + { + gender: 'female', + name: { + title: 'Miss', + first: 'Mattie', + last: 'Lambert', + }, + email: 'mattie.lambert@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mattie', + nat: 'AU', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Mijat', + last: 'Rakić', + }, + email: 'mijat.rakic@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mijat', + nat: 'RS', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Javier', + last: 'Reid', + }, + email: 'javier.reid@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Javier', + nat: 'US', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Isabella', + last: 'Li', + }, + email: 'isabella.li@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella', + nat: 'CA', + }, + { + gender: 'female', + name: { + title: 'Mrs', + first: 'Stephanie', + last: 'Garrett', + }, + email: 'stephanie.garrett@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Stephanie', + nat: 'AU', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Antonia', + last: 'Núñez', + }, + email: 'antonia.nunez@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Antonia', + nat: 'ES', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Donald', + last: 'Young', + }, + email: 'donald.young@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Donald', + nat: 'US', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Iegor', + last: 'Holodovskiy', + }, + email: 'iegor.holodovskiy@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Iegor', + nat: 'UA', + }, + { + gender: 'female', + name: { + title: 'Madame', + first: 'Jessica', + last: 'David', + }, + email: 'jessica.david@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jessica', + nat: 'CH', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Eve', + last: 'Martinez', + }, + email: 'eve.martinez@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Eve', + nat: 'FR', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Caleb', + last: 'Silva', + }, + email: 'caleb.silva@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Caleb', + nat: 'US', + }, + { + gender: 'female', + name: { + title: 'Miss', + first: 'Marcia', + last: 'Jenkins', + }, + email: 'marcia.jenkins@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Marcia', + nat: 'US', + }, + { + gender: 'female', + name: { + title: 'Mrs', + first: 'Mackenzie', + last: 'Jones', + }, + email: 'mackenzie.jones@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mackenzie', + nat: 'NZ', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Jeremiah', + last: 'Gutierrez', + }, + email: 'jeremiah.gutierrez@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jeremiah', + nat: 'AU', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Luciara', + last: 'Souza', + }, + email: 'luciara.souza@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Luciara', + nat: 'BR', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Valgi', + last: 'da Cunha', + }, + email: 'valgi.dacunha@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Valgi', + nat: 'BR', + }, + ], +}; + +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" + }; + email: string; // "duane.reed@example.com" + picture: string; // "https://api.dicebear.com/6.x/open-peeps/svg?seed=Duane" + nat: string; // "AU" +}; + +type DenseTableProps = { + users: User[]; +}; + +export const DenseTable = ({ users }: DenseTableProps) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { title: 'Avatar', field: 'avatar' }, + { title: 'Name', field: 'name' }, + { title: 'Email', field: 'email' }, + { title: 'Nationality', field: 'nationality' }, + ]; + + const data = users.map(user => { + return { + avatar: ( + {user.name.first} + ), + name: `${user.name.first} ${user.name.last}`, + email: user.email, + nationality: user.nat, + }; + }); + + return ( + + ); +}; + +export const ExampleFetchComponent = () => { + const { value, loading, error } = useAsync(async (): Promise => { + // Would use fetch in a real world example + return exampleUsers.results; + }, []); + + if (loading) { + return ; + } else if (error) { + return ; + } + + return ; +}; diff --git a/plugins/argocd/src/components/ExampleFetchComponent/index.ts b/plugins/argocd/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 0000000000..41a43e84f1 --- /dev/null +++ b/plugins/argocd/src/components/ExampleFetchComponent/index.ts @@ -0,0 +1 @@ +export { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/plugins/argocd/src/index.ts b/plugins/argocd/src/index.ts new file mode 100644 index 0000000000..75d4ad8eca --- /dev/null +++ b/plugins/argocd/src/index.ts @@ -0,0 +1 @@ +export { argocdPlugin, ArgocdPage } from './plugin'; diff --git a/plugins/argocd/src/plugin.test.ts b/plugins/argocd/src/plugin.test.ts new file mode 100644 index 0000000000..d9225b45bc --- /dev/null +++ b/plugins/argocd/src/plugin.test.ts @@ -0,0 +1,7 @@ +import { argocdPlugin } from './plugin'; + +describe('argocd', () => { + it('should export plugin', () => { + expect(argocdPlugin).toBeDefined(); + }); +}); diff --git a/plugins/argocd/src/plugin.ts b/plugins/argocd/src/plugin.ts new file mode 100644 index 0000000000..f96d1b03e3 --- /dev/null +++ b/plugins/argocd/src/plugin.ts @@ -0,0 +1,22 @@ +import { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +export const argocdPlugin = createPlugin({ + id: 'rh-argocd', + routes: { + root: rootRouteRef, + }, +}); + +export const ArgocdPage = argocdPlugin.provide( + createRoutableExtension({ + name: 'ArgocdPage', + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/argocd/src/routes.ts b/plugins/argocd/src/routes.ts new file mode 100644 index 0000000000..43c22cf1d6 --- /dev/null +++ b/plugins/argocd/src/routes.ts @@ -0,0 +1,5 @@ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'rh-argocd', +}); diff --git a/plugins/argocd/src/setupTests.ts b/plugins/argocd/src/setupTests.ts new file mode 100644 index 0000000000..7b0828bfa8 --- /dev/null +++ b/plugins/argocd/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/plugins/argocd/tsconfig.json b/plugins/argocd/tsconfig.json new file mode 100644 index 0000000000..a82a3f1e0d --- /dev/null +++ b/plugins/argocd/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src", "dev"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "../../dist-types/plugins/argocd", + "rootDir": "." + } +} diff --git a/plugins/argocd/turbo.json b/plugins/argocd/turbo.json new file mode 100644 index 0000000000..70074dc851 --- /dev/null +++ b/plugins/argocd/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "pipeline": { + "tsc": { + "outputs": ["../../dist-types/plugins/argocd/**"], + "dependsOn": ["^tsc"] + } + } +}