Add Octopus Deploy custom field extensions

Signed-off-by: Niek Rossen <niek.rossen@infosupport.com>
This commit is contained in:
Niek Rossen
2024-03-18 13:13:28 +01:00
parent 488c9c8675
commit 982440004d
9 changed files with 242 additions and 3 deletions
+40 -2
View File
@@ -41,7 +41,9 @@ octopusdeploy:
webBaseUrl: "<your-octopus-web-url>"
```
2. Add the following to `EntityPage.tsx` to display Octopus Releases
#### Adding the Entities
1. Add the following to `EntityPage.tsx` to display Octopus Releases
```
// In packages/app/src/components/catalog/EntityPage.tsx
@@ -60,7 +62,7 @@ const cicdContent = (
)
```
3. Add `octopus.com/project-id` annotation in the catalog descriptor file.
2. 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 +95,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
```
+4 -1
View File
@@ -37,9 +37,12 @@
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-scaffolder": "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"
"react-use": "^17.2.4",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
+32
View File
@@ -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>;
}
@@ -156,6 +164,30 @@ export class OctopusDeployClient implements OctopusDeployApi {
return responseJson;
}
async getProjectGroups(): Promise<OctopusProjectGroup[]> {
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
const url = `${proxyUrl}${this.proxyPathBase}/projectgroups/all`;
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;
}
async getConfig(): Promise<OctopusPluginConfig> {
return {
WebUiBaseUrl: this.configApi.getString(WEB_UI_BASE_URL_CONFIG_KEY),
@@ -0,0 +1,74 @@
/*
* 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 FormControl from '@material-ui/core/FormControl';
import { InputLabel, Input, FormHelperText } from '@material-ui/core';
import { Select, SelectItem } from '@backstage/core-components';
import { useProjectGroups } from '../../hooks/useProjectGroups';
import { ProjectGroupDropdownProps } from './schema';
export const ProjectGroupDropdown = ({
onChange,
rawErrors,
required,
formData,
}: ProjectGroupDropdownProps) => {
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 (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
>
{projectGroups?.length ? (
<Select
label="Project Group"
onChange={p => {
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 => onChange(e.target.value)}
value={formData}
/>
</>
)}
<FormHelperText>
The project group of the within Octopus Deploy.
</FormHelperText>
</FormControl>
);
};
@@ -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,25 @@
/*
* Copyright 2024 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 z from 'zod';
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
const ProjectGroupDropdownFieldSchema = makeFieldSchemaFromZod(z.string());
export const ProjectGroupDropdownSchema =
ProjectGroupDropdownFieldSchema.schema;
export type ProjectGroupDropdownProps =
typeof ProjectGroupDropdownFieldSchema.type;
@@ -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,
loading,
error,
};
}
+12
View File
@@ -27,7 +27,11 @@ 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';
import { ProjectGroupDropdownSchema } from './components/ScaffolderDropdown/schema';
/** @public */
export const isOctopusDeployAvailable = (entity: Entity) =>
@@ -61,3 +65,11 @@ export const EntityOctopusDeployContent = octopusDeployPlugin.provide(
mountPoint: octopusDeployEntityContentRouteRef,
}),
);
export const OctopusDeployDropdownFieldExtension = octopusDeployPlugin.provide(
createScaffolderFieldExtension({
name: 'OctopusDeployProjectGroupDropdown',
schema: ProjectGroupDropdownSchema,
component: ProjectGroupDropdown,
}),
);
+3
View File
@@ -7961,12 +7961,15 @@ __metadata:
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/plugin-scaffolder": "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
"@testing-library/react": ^14.0.0
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
react-use: ^17.2.4
zod: ^3.22.4
peerDependencies:
react: ^16.13.1 || ^17.0.0 || ^18.0.0
react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0