feat(DevSpacesPlugin): created a base plugin for DevSpaces

This commit is contained in:
2024-05-27 13:02:29 -04:00
parent b76fbfc34a
commit bf2b5ef867
20 changed files with 760 additions and 1 deletions

View File

@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

View File

@@ -0,0 +1,13 @@
# devspaces-plugin
Welcome to the devspaces-plugin 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 [/devspaces-plugin](http://localhost:3000/devspaces-plugin).
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.

View File

@@ -0,0 +1,12 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { devspacesPluginPlugin, DevspacesPluginPage } from '../src/plugin';
createDevApp()
.registerPlugin(devspacesPluginPlugin)
.addPage({
element: <DevspacesPluginPage />,
title: 'Root Page',
path: '/devspaces-plugin',
})
.render();

View File

@@ -0,0 +1,51 @@
{
"name": "@internal/backstage-plugin-devspaces-plugin",
"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": {
"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": "^0.14.4",
"@backstage/core-plugin-api": "^1.9.2",
"@backstage/theme": "^0.5.3",
"@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 || ^18.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.26.3",
"@backstage/core-app-api": "^1.12.4",
"@backstage/dev-utils": "^1.0.31",
"@backstage/test-utils": "^1.5.4",
"@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"
]
}

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { useEntity } from '@backstage/plugin-catalog-react'
import { Typography, Grid, Box } from '@material-ui/core';
import {
InfoCard,
Header,
Page,
Content,
ContentHeader,
HeaderLabel,
Link,
SupportButton,
Table,
} from '@backstage/core-components';
import { ExampleFetchComponent } from '../ExampleFetchComponent';
export const DevspacesComponent = () => {
const { entity } = useEntity();
const annotations = entity?.metadata?.annotations;
const clusterUrl = "oc-med.wk.nt.local"
const devspacesApp = "devspaces"
const devspacesLink = `https://${devspacesApp}.apps.${clusterUrl}/#https://github.com/${annotations?.['github.com/project-slug']}`;
return (
<Page themeId="">
<InfoCard>
{/* <Header title="Web IDE" /> */}
{/* <Content> */}
<h1>Open Shift Dev Spaces</h1>
<Link to={devspacesLink}>
Open project in VSCode Web IDE
</Link>
{/* </Content> */}
</InfoCard>
</Page>
);
};

View File

@@ -0,0 +1,38 @@
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 DevspacesComponent = () => (
<Page themeId="tool">
<Header title="Welcome to devspaces-plugin!" subtitle="Optional subtitle">
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title="Plugin title">
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title="Information card">
<Typography variant="body1">
All content should be wrapped in a card like this.
</Typography>
</InfoCard>
</Grid>
<Grid item>
<ExampleFetchComponent />
</Grid>
</Grid>
</Content>
</Page>
);

View File

@@ -0,0 +1,29 @@
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(<ExampleComponent />);
expect(
screen.getByText('Welcome to devspaces-plugin!'),
).toBeInTheDocument();
});
});

View File

@@ -0,0 +1 @@
export { DevspacesComponent } from './DevspacesComponent';

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { ExampleFetchComponent } from './ExampleFetchComponent';
describe('ExampleFetchComponent', () => {
it('renders the user table', async () => {
render(<ExampleFetchComponent />);
// 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();
});
});

View File

@@ -0,0 +1,309 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import {
Table,
TableColumn,
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import useAsync from 'react-use/lib/useAsync';
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: (
<img
src={user.picture}
className={classes.avatar}
alt={user.name.first}
/>
),
name: `${user.name.first} ${user.name.last}`,
email: user.email,
nationality: user.nat,
};
});
return (
<Table
title="Example User List"
options={{ search: false, paging: false }}
columns={columns}
data={data}
/>
);
};
export const ExampleFetchComponent = () => {
const { value, loading, error } = useAsync(async (): Promise<User[]> => {
// Would use fetch in a real world example
return exampleUsers.results;
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <ResponseErrorPanel error={error} />;
}
return <DenseTable users={value || []} />;
};

View File

@@ -0,0 +1 @@
export { ExampleFetchComponent } from './ExampleFetchComponent';

View File

@@ -0,0 +1 @@
export { devspacesPluginPlugin, DevspacesPluginPage } from './plugin';

View File

@@ -0,0 +1,7 @@
import { devspacesPluginPlugin } from './plugin';
describe('devspaces-plugin', () => {
it('should export plugin', () => {
expect(devspacesPluginPlugin).toBeDefined();
});
});

View File

@@ -0,0 +1,22 @@
import {
createPlugin,
createRoutableExtension,
} from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';
export const devspacesPluginPlugin = createPlugin({
id: 'devspaces-plugin',
routes: {
root: rootRouteRef,
},
});
export const DevspacesPluginPage = devspacesPluginPlugin.provide(
createRoutableExtension({
name: 'DevspacesPluginPage',
component: () =>
import('./components/DevspacesComponent').then(m => m.DevspacesComponent),
mountPoint: rootRouteRef,
}),
);

View File

@@ -0,0 +1,5 @@
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'devspaces-plugin',
});

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom';