Merge pull request #23683 from NiekRossenInfoSupport/feature/octopus-project-group-dropdown
Feature: Project group dropdown for Octopus Deploy
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-octopus-deploy': patch
|
||||
---
|
||||
|
||||
Added dropdown field extension for Octopus Deploy project groups
|
||||
@@ -23,15 +23,16 @@ This plugin (currently) uses the Backstage proxy to securely communicate with th
|
||||
|
||||
To use it, you will need to generate an [API Key](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) within Octopus Deploy.
|
||||
|
||||
1. Add the following to your app-config.yaml to enable the proxy:
|
||||
Add the following to your app-config.yaml to enable the proxy:
|
||||
|
||||
```
|
||||
// app-config.yaml
|
||||
proxy:
|
||||
'/octopus-deploy':
|
||||
target: 'https://<your-octopus-server-instance>/api'
|
||||
headers:
|
||||
X-Octopus-ApiKey: ${OCTOPUS_API_KEY}
|
||||
endpoints:
|
||||
'/octopus-deploy':
|
||||
target: 'https://<your-octopus-server-instance>/api'
|
||||
headers:
|
||||
X-Octopus-ApiKey: ${OCTOPUS_API_KEY}
|
||||
```
|
||||
|
||||
Optionally, also add the following section to your app-config.yaml if you wish to enable linking to the Project Release page in the Octopus Deploy UI from the footer of the Backstage Release Table. Typically this will be the server URL above without the /api postfix.
|
||||
@@ -41,7 +42,9 @@ octopusdeploy:
|
||||
webBaseUrl: "<your-octopus-web-url>"
|
||||
```
|
||||
|
||||
2. Add the following to `EntityPage.tsx` to display Octopus Releases
|
||||
#### Adding the Entities
|
||||
|
||||
Add the following to `EntityPage.tsx` to display Octopus Releases
|
||||
|
||||
```
|
||||
// In packages/app/src/components/catalog/EntityPage.tsx
|
||||
@@ -60,7 +63,7 @@ const cicdContent = (
|
||||
)
|
||||
```
|
||||
|
||||
3. Add `octopus.com/project-id` annotation in the catalog descriptor file.
|
||||
Add `octopus.com/project-id` annotation in the catalog descriptor file.
|
||||
|
||||
To obtain a projects ID you will have to query the Octopus API. In the future we'll add support for using a projects slug as well.
|
||||
|
||||
@@ -93,3 +96,39 @@ spec:
|
||||
You can get the ID of the space from the URL in the Octopus Deploy UI.
|
||||
|
||||
All set, you will be able to see the plugin in action!
|
||||
|
||||
### Adding Scaffolder field extensions
|
||||
|
||||
To add the Octopus Deploy custom fields extensions, add the following to your `App.tsx`:
|
||||
|
||||
```tsx
|
||||
// In packages/app/src/App.tsx
|
||||
import { OctopusDeployDropdownFieldExtension } from '@backstage/plugin-octopus-deploy';
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
...
|
||||
<Route path="/create" element={<ScaffolderPage />}>
|
||||
<ScaffolderFieldExtensions>
|
||||
<OctopusDeployDropdownFieldExtension />
|
||||
</ScaffolderFieldExtensions>
|
||||
</Route>
|
||||
...
|
||||
</FlatRoutes>
|
||||
```
|
||||
|
||||
To use the Octopus Deploy custom field extensions, add the following to your `template.yaml`:
|
||||
|
||||
```yaml
|
||||
# In the template.yaml
|
||||
apiVersion: scaffolder.backstage.io/v1beta3
|
||||
kind: Template
|
||||
metadata:
|
||||
# ...
|
||||
parameters:
|
||||
- title: Octopus Project Group
|
||||
properties:
|
||||
projectName:
|
||||
title: Octopus Project Group
|
||||
type: string
|
||||
ui:field: OctopusDeployProjectGroupDropdown
|
||||
```
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { FieldExtensionComponent } from '@backstage/plugin-scaffolder-react';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -29,6 +30,8 @@ export interface OctopusDeployApi {
|
||||
// (undocumented)
|
||||
getConfig(): Promise<OctopusPluginConfig>;
|
||||
// (undocumented)
|
||||
getProjectGroups(): Promise<OctopusProjectGroup[]>;
|
||||
// (undocumented)
|
||||
getProjectInfo(projectReference: ProjectReference): Promise<OctopusProject>;
|
||||
// (undocumented)
|
||||
getReleaseProgression(opts: {
|
||||
@@ -51,6 +54,8 @@ export class OctopusDeployClient implements OctopusDeployApi {
|
||||
// (undocumented)
|
||||
getConfig(): Promise<OctopusPluginConfig>;
|
||||
// (undocumented)
|
||||
getProjectGroups(): Promise<OctopusProjectGroup[]>;
|
||||
// (undocumented)
|
||||
getProjectInfo(projectReference: ProjectReference): Promise<OctopusProject>;
|
||||
// (undocumented)
|
||||
getReleaseProgression(opts: {
|
||||
@@ -59,6 +64,12 @@ export class OctopusDeployClient implements OctopusDeployApi {
|
||||
}): Promise<OctopusProgression>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const OctopusDeployDropdownFieldExtension: FieldExtensionComponent<
|
||||
string,
|
||||
{}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type OctopusDeployment = {
|
||||
State: string;
|
||||
@@ -97,6 +108,13 @@ export type OctopusProject = {
|
||||
Links: OctopusLinks;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type OctopusProjectGroup = {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Description: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type OctopusRelease = {
|
||||
Id: string;
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-react": "workspace:^",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react-use": "^17.2.4"
|
||||
|
||||
@@ -62,6 +62,13 @@ export type OctopusProject = {
|
||||
Links: OctopusLinks;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type OctopusProjectGroup = {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Description: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type OctopusPluginConfig = {
|
||||
WebUiBaseUrl: string;
|
||||
@@ -82,6 +89,7 @@ export interface OctopusDeployApi {
|
||||
releaseHistoryCount: number;
|
||||
}): Promise<OctopusProgression>;
|
||||
getProjectInfo(projectReference: ProjectReference): Promise<OctopusProject>;
|
||||
getProjectGroups(): Promise<OctopusProjectGroup[]>;
|
||||
getConfig(): Promise<OctopusPluginConfig>;
|
||||
}
|
||||
|
||||
@@ -109,51 +117,19 @@ export class OctopusDeployClient implements OctopusDeployApi {
|
||||
releaseHistoryCount: number;
|
||||
}): Promise<OctopusProgression> {
|
||||
const url = await this.getProgressionApiUrl(opts);
|
||||
|
||||
const response = await this.fetchApi.fetch(url);
|
||||
|
||||
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;
|
||||
return this.fetchAndHandleErrors(url);
|
||||
}
|
||||
|
||||
async getProjectInfo(
|
||||
projectReference: ProjectReference,
|
||||
): Promise<OctopusProject> {
|
||||
const url = await this.getProjectApiUrl(projectReference);
|
||||
const response = await this.fetchApi.fetch(url);
|
||||
return this.fetchAndHandleErrors(url);
|
||||
}
|
||||
|
||||
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;
|
||||
async getProjectGroups(): Promise<OctopusProjectGroup[]> {
|
||||
const url = await this.getProjectGroupApiUrl();
|
||||
return this.fetchAndHandleErrors(url);
|
||||
}
|
||||
|
||||
async getConfig(): Promise<OctopusPluginConfig> {
|
||||
@@ -162,6 +138,26 @@ export class OctopusDeployClient implements OctopusDeployApi {
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchAndHandleErrors<T>(url: string): Promise<T> {
|
||||
const response = await this.fetchApi.fetch(url);
|
||||
|
||||
let responseJson: T;
|
||||
|
||||
try {
|
||||
responseJson = await response.json();
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse JSON response: ${e}`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Error communicating with Octopus Deploy: ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
private async getProgressionApiUrl(opts: {
|
||||
projectReference: ProjectReference;
|
||||
releaseHistoryCount: number;
|
||||
@@ -185,4 +181,9 @@ export class OctopusDeployClient implements OctopusDeployApi {
|
||||
projectReference.projectId,
|
||||
)}`;
|
||||
}
|
||||
|
||||
private async getProjectGroupApiUrl(): Promise<string> {
|
||||
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
|
||||
return `${proxyUrl}${this.proxyPathBase}/projectgroups/all`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 { InputLabel, Input } from '@material-ui/core';
|
||||
import { Select, SelectItem } from '@backstage/core-components';
|
||||
import { useProjectGroups } from '../../hooks/useProjectGroups';
|
||||
import { ScaffolderField } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
export const ProjectGroupDropdown = ({
|
||||
onChange,
|
||||
errors,
|
||||
rawErrors,
|
||||
required,
|
||||
help,
|
||||
rawDescription,
|
||||
disabled,
|
||||
}: FieldExtensionComponentProps<string>) => {
|
||||
const [selectedProjectGroup, setSelectedProjectGroup] =
|
||||
React.useState<string>('');
|
||||
const { projectGroups } = useProjectGroups();
|
||||
|
||||
const projectGroupItems: SelectItem[] = projectGroups
|
||||
? projectGroups.map(pg => ({ label: pg.Name, value: pg.Name }))
|
||||
: [];
|
||||
|
||||
const updateProjectGroup = (pg: string) => {
|
||||
setSelectedProjectGroup(pg);
|
||||
onChange(pg);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScaffolderField
|
||||
rawErrors={rawErrors}
|
||||
errors={errors}
|
||||
required={required}
|
||||
help={help}
|
||||
rawDescription={rawDescription}
|
||||
disabled={disabled}
|
||||
>
|
||||
{projectGroups?.length ? (
|
||||
<Select
|
||||
label="Project Group"
|
||||
onChange={p => {
|
||||
// param p is an array if the select is multi-select,
|
||||
// since this is single-select we can just take the first element
|
||||
updateProjectGroup(String(Array.isArray(p) ? p[0] : p));
|
||||
}}
|
||||
disabled={projectGroups.length === 1}
|
||||
selected={selectedProjectGroup}
|
||||
items={projectGroupItems}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<InputLabel>Project Group</InputLabel>
|
||||
<Input
|
||||
id="project-group-input"
|
||||
onChange={e => updateProjectGroup(e.target.value)}
|
||||
value={selectedProjectGroup}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ScaffolderField>
|
||||
);
|
||||
};
|
||||
@@ -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 { ProjectGroupDropdown } from './ProjectGroupDropdown';
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 { useApi } from '@backstage/core-plugin-api';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { octopusDeployApiRef, OctopusProjectGroup } from '../api';
|
||||
|
||||
export function useProjectGroups(): {
|
||||
projectGroups: OctopusProjectGroup[];
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
} {
|
||||
const api = useApi(octopusDeployApiRef);
|
||||
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
return api.getProjectGroups();
|
||||
}, [api]);
|
||||
|
||||
return {
|
||||
projectGroups: value ? value : [],
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
export {
|
||||
octopusDeployPlugin,
|
||||
EntityOctopusDeployContent,
|
||||
OctopusDeployDropdownFieldExtension,
|
||||
isOctopusDeployAvailable,
|
||||
} from './plugin';
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ import {
|
||||
configApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ProjectGroupDropdown } from './components/ScaffolderDropdown';
|
||||
|
||||
/** @public */
|
||||
export const isOctopusDeployAvailable = (entity: Entity) =>
|
||||
@@ -61,3 +64,11 @@ export const EntityOctopusDeployContent = octopusDeployPlugin.provide(
|
||||
mountPoint: octopusDeployEntityContentRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const OctopusDeployDropdownFieldExtension = octopusDeployPlugin.provide(
|
||||
createScaffolderFieldExtension({
|
||||
name: 'OctopusDeployProjectGroupDropdown',
|
||||
component: ProjectGroupDropdown,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -8000,6 +8000,7 @@ __metadata:
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/dev-utils": "workspace:^"
|
||||
"@backstage/plugin-catalog-react": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-react": "workspace:^"
|
||||
"@material-ui/core": ^4.9.13
|
||||
"@testing-library/dom": ^9.0.0
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
|
||||
Reference in New Issue
Block a user