Some initial code

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>
This commit is contained in:
Jonathan Mezach
2023-02-15 10:19:39 +01:00
parent 4323348512
commit f9b1ec31f1
17 changed files with 415 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+11
View File
@@ -0,0 +1,11 @@
# Octopus Deploy Plugin
Welcome to the octopus-deploy plugin!
## Features
- Display the deployment status of the most recent releases for a project in Octopus Deploy straight from the Backstage catalog
## Getting started
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { octopusDeployPlugin, OctopusDeployPage } from '../src/plugin';
createDevApp()
.registerPlugin(octopusDeployPlugin)
.addPage({
element: <OctopusDeployPage />,
title: 'Root Page',
path: '/octopus-deploy'
})
.render();
+52
View File
@@ -0,0 +1,52 @@
{
"name": "@backstage/plugin-octopus-deploy",
"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"
},
"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.12.3",
"@backstage/core-plugin-api": "^1.3.0",
"@backstage/theme": "^0.2.16",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.22.1",
"@backstage/core-app-api": "^1.4.0",
"@backstage/dev-utils": "^1.0.11",
"@backstage/test-utils": "^1.2.4",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"msw": "^0.49.0",
"cross-fetch": "^3.1.5"
},
"files": [
"dist"
]
}
+88
View File
@@ -0,0 +1,88 @@
import { createApiRef, DiscoveryApi, IdentityApi } from "@backstage/core-plugin-api";
export type OctopusProgression = {
Environments: OctopusEnvironment[];
Releases: OctopusReleaseProgression[];
}
export type OctopusEnvironment = {
Id: string,
Name: string
}
export type OctopusReleaseProgression = {
Release: OctopusRelease
Deployments: { [key: string]: OctopusDeployment[] }
}
export type OctopusRelease = {
Id: string
Version: string
}
export type OctopusDeployment = {
State: string
}
export const octopusDeployApiRef = createApiRef<OctopusDeployApi>({
id: 'plugin.octopusdeploy.service'
});
const DEFAULT_PROXY_PATH_BASE = '/octopus-deploy';
type Options = {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi
/**
* Path to use for requests via the proxy, defaults to /octopus-deploy
*/
proxyPathBase?: string
}
export interface OctopusDeployApi {
getReleaseProgression(projectId: string, releaseHistoryCount: number): Promise<OctopusProgression>;
}
export class OctopusDeployClient implements OctopusDeployApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly proxyPathBase: string;
constructor(options: Options) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
this.proxyPathBase = options.proxyPathBase ?? DEFAULT_PROXY_PATH_BASE;
}
async getReleaseProgression(projectId: string, releaseHistoryCount: number): Promise<OctopusProgression> {
const url = await this.getApiUrl(projectId, releaseHistoryCount);
const { token: idToken } = await this.identityApi.getCredentials();
const response = await fetch(url, {
headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
});
let responseJson;
try {
responseJson = await response.json();
} catch (e) {
responseJson = { releases: [] };
}
if (response.status !== 200) {
throw new Error(
`Error communicating with Octopus Deploy: ${
responseJson?.error?.title || response.statusText
}`,
);
}
return responseJson;
}
private async getApiUrl(projectId: string, releaseHistoryCount: number) {
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
return `${proxyUrl}${this.proxyPathBase}/projects/${projectId}/progression?releaseHistoryCount=${releaseHistoryCount}`;
}
}
@@ -0,0 +1,15 @@
import { useEntity } from '@backstage/plugin-catalog-react';
import { useReleases } from '../../hooks/useReleases';
import { getAnnotationFromEntity } from '../../utils/getAnnotationFromEntity';
import React from 'react';
import { ReleaseTable } from '../ReleaseTable';
export const EntityPageOctopusDeploy = (props: { defaultLimit?: number }) => {
const { entity } = useEntity();
const projectId = getAnnotationFromEntity(entity);
const { environments, releases, loading, error } = useReleases(projectId, props.defaultLimit ?? 3);
return <ReleaseTable environments={environments} releases={releases} loading={loading} error={error} />;
}
@@ -0,0 +1 @@
export { EntityPageOctopusDeploy } from './EntityPageOctopusDeploy';
@@ -0,0 +1,123 @@
import { Box, Typography } from "@material-ui/core";
import {
OctopusEnvironment,
OctopusReleaseProgression
} from '../../api';
import React from 'react';
import { ResponseErrorPanel, StatusAborted, StatusError, StatusOK, StatusPending, StatusRunning, StatusWarning, Table, TableColumn } from "@backstage/core-components";
type ReleaseTableProps = {
environments?: OctopusEnvironment[];
releases?: OctopusReleaseProgression[];
loading: boolean;
error?: Error
};
export const getDeploymentStatusComponent = (state: string | undefined) => {
switch (state) {
case "Success":
return (
<Typography component="span">
<StatusOK /> Success
</Typography>
);
case "Queued":
return (
<Typography component="span">
<StatusPending /> Queued
</Typography>
);
case "Executing":
return (
<Typography component="span">
<StatusRunning /> Executing
</Typography>
);
case "Failed":
return (
<Typography component="span">
<StatusError /> Failed
</Typography>
);
case "Cancelling":
return (
<Typography component="span">
<StatusPending /> Cancelling
</Typography>
);
case "Canceled":
return (
<Typography component="span">
<StatusAborted /> Canceled
</Typography>
);
case "TimedOut":
return (
<Typography component="span">
<StatusWarning /> Timed Out
</Typography>
);
default:
return (
<Typography component="span">
<StatusWarning /> Unknown
</Typography>
);
}
}
export const ReleaseTable = ({environments, releases, loading, error}: ReleaseTableProps) => {
if (error) {
return (
<div>
<ResponseErrorPanel error={error} />
</div>
);
}
const columns: TableColumn[] = [
{
title: 'Version',
field: 'Release.Version',
highlight: false,
width: 'auto'
},
...environments?.map(env => ({
title: env.Name,
width: 'auto',
render: (row: Partial<OctopusReleaseProgression>) => {
const deploymentsForEnvironment = row.Deployments[env.Id];
if (deploymentsForEnvironment) {
return (
<Box display="flex" alignItems="center">
<Typography variant="button">
{getDeploymentStatusComponent(deploymentsForEnvironment[0].State)}
</Typography>
</Box>
)
}
}
//field: `Deployments.${env.Id}[0].State` // TODO: We'll probably need to deal with multi-tenant deployments here
})) ?? []
]
return (
<Table
isLoading={loading}
columns={columns}
options={{
search: true,
paging: true,
pageSize: 5,
showEmptyDataSourceMessage: !loading
}}
title={
<Box display="flex" alignItems="center">
Octopus Deploy - Releases ({releases ? releases.length : 0})
</Box>
}
data={releases ?? []}
/>
)
}
@@ -0,0 +1 @@
export { ReleaseTable } from './ReleaseTable';
+1
View File
@@ -0,0 +1 @@
export const OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION = 'octopus.com/project-id';
@@ -0,0 +1,26 @@
import { useApi } from "@backstage/core-plugin-api";
import useAsync from "react-use/lib/useAsync";
import { octopusDeployApiRef, OctopusEnvironment, OctopusReleaseProgression } from "../api";
export function useReleases(
projectId: string,
releaseHistoryCount: number
): {
environments?: OctopusEnvironment[],
releases?: OctopusReleaseProgression[]
loading: boolean;
error?: Error;
} {
const api = useApi(octopusDeployApiRef);
const { value, loading, error } = useAsync(() => {
return api.getReleaseProgression(projectId, releaseHistoryCount);
}, [ api, projectId, releaseHistoryCount ]);
return {
environments: value?.Environments,
releases: value?.Releases,
loading,
error
};
}
+7
View File
@@ -0,0 +1,7 @@
export {
octopusDeployPlugin,
EntityOctopusDeployContent,
isOctopusDeployAvailable
} from './plugin';
export * from './api';
@@ -0,0 +1,7 @@
import { octopusDeployPlugin } from './plugin';
describe('octopus-deploy', () => {
it('should export plugin', () => {
expect(octopusDeployPlugin).toBeDefined();
});
});
+49
View File
@@ -0,0 +1,49 @@
import {
OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION
} from './constants';
import {
octopusDeployEntityContentRouteRef
} from './routes';
import { OctopusDeployClient, octopusDeployApiRef } from './api';
import { createApiFactory, createPlugin, createRoutableExtension, discoveryApiRef, identityApiRef } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
/** @public */
export const isOctopusDeployAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION]);
export const octopusDeployPlugin = createPlugin({
id: 'octopus-deploy',
apis: [
createApiFactory({
api: octopusDeployApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) => new OctopusDeployClient({ discoveryApi, identityApi })
})
]
});
/*
export const OctopusDeployPage = octopusDeployPlugin.provide(
createRoutableExtension({
name: 'OctopusDeployPage',
component: () =>
import('./components/ExampleComponent').then(m => m.ExampleComponent),
mountPoint: rootRouteRef,
}),
);
*/
export const EntityOctopusDeployContent = octopusDeployPlugin.provide(
createRoutableExtension({
name: 'EntityOctopusDeployContent',
component: () =>
import('./components/EntityPageOctopusDeploy').then(
m => m.EntityPageOctopusDeploy
),
mountPoint: octopusDeployEntityContentRouteRef
})
)
+5
View File
@@ -0,0 +1,5 @@
import { createRouteRef } from '@backstage/core-plugin-api';
export const octopusDeployEntityContentRouteRef = createRouteRef({
id: 'octopus-deploy-entity-content'
})
+2
View File
@@ -0,0 +1,2 @@
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
@@ -0,0 +1,14 @@
import {
OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION
} from '../constants';
import { Entity } from '@backstage/catalog-model';
export function getAnnotationFromEntity(entity: Entity): string {
const annotation = entity.metadata.annotations?.[OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION];
if (!annotation) {
throw new Error('Value for annotation octopus.com/project-id was not found');
}
return annotation;
}