Added deep link to Octopus Deploy Project from release table for Octopus Deploy plugin

Signed-off-by: Graeme Christie <gchristie@bunnings.com.au>
This commit is contained in:
Graeme Christie
2023-10-24 13:23:21 +08:00
parent a123455ce3
commit 4d2f72c572
10 changed files with 258 additions and 38 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-octopus-deploy': minor
---
Added Deep link into Octopus Deploy project from the Release Table.
+7
View File
@@ -23,6 +23,13 @@ proxy:
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.
```
octopusdeploy:
webBaseUrl: "<your-octopus-web-url>"
```
2. Add the following to `EntityPage.tsx` to display Octopus Releases
```
+24
View File
@@ -0,0 +1,24 @@
/*
* 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 interface Config {
octopusdeploy?: {
/**
* Frontend Web UI Base URL for deep links to UI
* @visibility frontend
*/
webBaseUrl?: string;
};
}
+10 -2
View File
@@ -1,8 +1,14 @@
{
"name": "@backstage/plugin-octopus-deploy",
<<<<<<< HEAD
"version": "0.2.8-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
=======
"version": "0.2.7",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts",
>>>>>>> e7e3614866b7 (Added deep link to Octopus Deploy Project from release table for Octopus Deploy plugin)
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
@@ -57,6 +63,8 @@
"msw": "^1.0.0"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
+70 -12
View File
@@ -17,6 +17,7 @@ import {
createApiRef,
DiscoveryApi,
FetchApi,
ConfigApi,
} from '@backstage/core-plugin-api';
import { ProjectReference } from '../utils/getAnnotationFromEntity';
@@ -48,6 +49,23 @@ export type OctopusRelease = {
export type OctopusDeployment = {
State: string;
};
/** @public */
export type OctopusLinks = {
Self: string;
Web: string;
};
/** @public */
export type OctopusProject = {
Name: string;
Slug: string;
Links: OctopusLinks;
};
/** @public */
export type OctopusPluginConfig = {
WebUiBaseUrl: string;
};
/** @public */
export const octopusDeployApiRef = createApiRef<OctopusDeployApi>({
@@ -55,6 +73,7 @@ export const octopusDeployApiRef = createApiRef<OctopusDeployApi>({
});
const DEFAULT_PROXY_PATH_BASE = '/octopus-deploy';
const WEB_UI_BASE_URL_CONFIG_KEY = 'octopusdeploy.webBaseUrl';
/** @public */
export interface OctopusDeployApi {
@@ -62,19 +81,24 @@ export interface OctopusDeployApi {
projectReference: ProjectReference;
releaseHistoryCount: number;
}): Promise<OctopusProgression>;
getProjectInfo(projectReference: ProjectReference): Promise<OctopusProject>;
getConfig(): Promise<OctopusPluginConfig>;
}
/** @public */
export class OctopusDeployClient implements OctopusDeployApi {
private readonly configApi: ConfigApi;
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
private readonly proxyPathBase: string;
constructor(options: {
configApi: ConfigApi;
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
proxyPathBase?: string;
}) {
this.configApi = options.configApi;
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
this.proxyPathBase = options.proxyPathBase ?? DEFAULT_PROXY_PATH_BASE;
@@ -84,7 +108,7 @@ export class OctopusDeployClient implements OctopusDeployApi {
projectReference: ProjectReference;
releaseHistoryCount: number;
}): Promise<OctopusProgression> {
const url = await this.getApiUrl(opts);
const url = await this.getProgressionApiUrl(opts);
const response = await this.fetchApi.fetch(url);
@@ -107,24 +131,58 @@ export class OctopusDeployClient implements OctopusDeployApi {
return responseJson;
}
private async getApiUrl(opts: {
async getProjectInfo(
projectReference: ProjectReference,
): Promise<OctopusProject> {
const url = await this.getProjectApiUrl(projectReference);
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),
};
}
private async getProgressionApiUrl(opts: {
projectReference: ProjectReference;
releaseHistoryCount: number;
}) {
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
const queryParameters = new URLSearchParams({
releaseHistoryCount: opts.releaseHistoryCount.toString(),
});
if (opts.projectReference.spaceId !== undefined) {
return `${proxyUrl}${this.proxyPathBase}/${encodeURIComponent(
opts.projectReference.spaceId,
)}/projects/${encodeURIComponent(
opts.projectReference.projectId,
)}/progression?${queryParameters}`;
}
const projectUrl = await this.getProjectApiUrl(opts.projectReference);
return `${projectUrl}/progression?${queryParameters}`;
}
private async getProjectApiUrl(projectReference: ProjectReference) {
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
if (projectReference.spaceId !== undefined)
return `${proxyUrl}${this.proxyPathBase}/${encodeURIComponent(
projectReference.spaceId,
)}/projects/${encodeURIComponent(projectReference.projectId)}`;
return `${proxyUrl}${this.proxyPathBase}/projects/${encodeURIComponent(
opts.projectReference.projectId,
)}/progression?${queryParameters}`;
projectReference.projectId,
)}`;
}
}
@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useConfig } from '../../hooks/useConfig';
import { useEntity } from '@backstage/plugin-catalog-react';
import { useProject } from '../../hooks/useProject';
import { useReleases } from '../../hooks/useReleases';
import { getProjectReferenceAnnotationFromEntity } from '../../utils/getAnnotationFromEntity';
import React from 'react';
@@ -30,12 +32,22 @@ export const EntityPageOctopusDeploy = (props: { defaultLimit?: number }) => {
projectReference.spaceId,
);
const {
project,
loading: projectLoading,
error: projectError,
} = useProject(projectReference.projectId, projectReference.spaceId);
const { config, loading: configLoading, error: configError } = useConfig();
return (
<ReleaseTable
environments={environments}
releases={releases}
loading={loading}
error={error}
project={project}
config={config}
loading={loading || projectLoading || configLoading}
error={error || projectError || configError}
/>
);
};
@@ -14,10 +14,17 @@
* limitations under the License.
*/
import { Box, Typography } from '@material-ui/core';
import { OctopusEnvironment, OctopusReleaseProgression } from '../../api';
import {
OctopusEnvironment,
OctopusProject,
OctopusReleaseProgression,
OctopusPluginConfig,
} from '../../api';
import React from 'react';
import {
BottomLink,
BottomLinkProps,
ResponseErrorPanel,
StatusAborted,
StatusError,
@@ -33,6 +40,8 @@ import { OctopusDeployIcon } from '../OctopusDeployIcon';
type ReleaseTableProps = {
environments?: OctopusEnvironment[];
releases?: OctopusReleaseProgression[];
project?: OctopusProject;
config?: OctopusPluginConfig;
loading: boolean;
error?: Error;
};
@@ -93,6 +102,8 @@ export const getDeploymentStatusComponent = (state: string | undefined) => {
export const ReleaseTable = ({
environments,
releases,
project,
config,
loading,
error,
}: ReleaseTableProps) => {
@@ -130,24 +141,39 @@ export const ReleaseTable = ({
})) ?? []),
];
const deepLink: BottomLinkProps | null =
project?.Links?.Web && config?.WebUiBaseUrl
? {
link: `${config.WebUiBaseUrl}${project.Links.Web}`,
title: 'Go to project',
onClick: e => {
e.preventDefault();
window.open(`${config?.WebUiBaseUrl}${project?.Links.Web}`);
},
}
: null;
return (
<Table
isLoading={loading}
columns={columns}
options={{
search: true,
paging: true,
pageSize: 5,
showEmptyDataSourceMessage: !loading,
}}
title={
<Box display="flex" alignItems="center">
<OctopusDeployIcon style={{ fontSize: 30 }} />
<Box mr={1} />
Octopus Deploy - Releases ({releases ? releases.length : 0})
</Box>
}
data={releases ?? []}
/>
<Box>
<Table
isLoading={loading}
columns={columns}
options={{
search: true,
paging: true,
pageSize: 5,
showEmptyDataSourceMessage: !loading,
}}
title={
<Box display="flex" alignItems="center">
<OctopusDeployIcon style={{ fontSize: 30 }} />
<Box mr={1} />
Octopus Deploy - Releases ({releases ? releases.length : 0})
</Box>
}
data={releases ?? []}
/>
{deepLink !== null && <BottomLink {...deepLink} />}
</Box>
);
};
@@ -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, OctopusPluginConfig } from '../api';
export function useConfig(): {
config?: OctopusPluginConfig;
loading: boolean;
error?: Error;
} {
const api = useApi(octopusDeployApiRef);
const { value, loading, error } = useAsync(() => {
return api.getConfig();
}, [api]);
return {
config: value,
loading,
error,
};
}
@@ -0,0 +1,39 @@
/*
* 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, OctopusProject } from '../api';
export function useProject(
projectId: string,
spaceId?: string,
): {
project?: OctopusProject;
loading: boolean;
error?: Error;
} {
const api = useApi(octopusDeployApiRef);
const { value, loading, error } = useAsync(() => {
return api.getProjectInfo({ projectId: projectId, spaceId: spaceId });
}, [api, projectId, spaceId]);
return {
project: value,
loading,
error,
};
}
+8 -3
View File
@@ -24,6 +24,7 @@ import {
createRoutableExtension,
discoveryApiRef,
fetchApiRef,
configApiRef,
} from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
@@ -38,9 +39,13 @@ export const octopusDeployPlugin = createPlugin({
apis: [
createApiFactory({
api: octopusDeployApiRef,
deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
factory: ({ discoveryApi, fetchApi }) =>
new OctopusDeployClient({ discoveryApi, fetchApi }),
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, fetchApi, configApi }) =>
new OctopusDeployClient({ discoveryApi, fetchApi, configApi }),
}),
],
});