From 4d2f72c572365a66e8ce2799bd8ac251cbf2d873 Mon Sep 17 00:00:00 2001 From: Graeme Christie Date: Tue, 24 Oct 2023 13:23:21 +0800 Subject: [PATCH] Added deep link to Octopus Deploy Project from release table for Octopus Deploy plugin Signed-off-by: Graeme Christie --- .changeset/tender-donkeys-speak.md | 5 ++ plugins/octopus-deploy/README.md | 7 ++ plugins/octopus-deploy/config.d.ts | 24 ++++++ plugins/octopus-deploy/package.json | 12 ++- plugins/octopus-deploy/src/api/index.ts | 82 ++++++++++++++++--- .../EntityPageOctopusDeploy.tsx | 16 +++- .../components/ReleaseTable/ReleaseTable.tsx | 64 ++++++++++----- plugins/octopus-deploy/src/hooks/useConfig.ts | 36 ++++++++ .../octopus-deploy/src/hooks/useProject.ts | 39 +++++++++ plugins/octopus-deploy/src/plugin.ts | 11 ++- 10 files changed, 258 insertions(+), 38 deletions(-) create mode 100644 .changeset/tender-donkeys-speak.md create mode 100644 plugins/octopus-deploy/config.d.ts create mode 100644 plugins/octopus-deploy/src/hooks/useConfig.ts create mode 100644 plugins/octopus-deploy/src/hooks/useProject.ts diff --git a/.changeset/tender-donkeys-speak.md b/.changeset/tender-donkeys-speak.md new file mode 100644 index 0000000000..73a72aa427 --- /dev/null +++ b/.changeset/tender-donkeys-speak.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-octopus-deploy': minor +--- + +Added Deep link into Octopus Deploy project from the Release Table. diff --git a/plugins/octopus-deploy/README.md b/plugins/octopus-deploy/README.md index e80bbf0fcd..9e37777091 100644 --- a/plugins/octopus-deploy/README.md +++ b/plugins/octopus-deploy/README.md @@ -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: "" +``` + 2. Add the following to `EntityPage.tsx` to display Octopus Releases ``` diff --git a/plugins/octopus-deploy/config.d.ts b/plugins/octopus-deploy/config.d.ts new file mode 100644 index 0000000000..81d14f2921 --- /dev/null +++ b/plugins/octopus-deploy/config.d.ts @@ -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; + }; +} diff --git a/plugins/octopus-deploy/package.json b/plugins/octopus-deploy/package.json index 6949d1c035..1eee223008 100644 --- a/plugins/octopus-deploy/package.json +++ b/plugins/octopus-deploy/package.json @@ -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" } diff --git a/plugins/octopus-deploy/src/api/index.ts b/plugins/octopus-deploy/src/api/index.ts index 0364bed269..cb5d428308 100644 --- a/plugins/octopus-deploy/src/api/index.ts +++ b/plugins/octopus-deploy/src/api/index.ts @@ -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({ @@ -55,6 +73,7 @@ export const octopusDeployApiRef = createApiRef({ }); 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; + getProjectInfo(projectReference: ProjectReference): Promise; + getConfig(): Promise; } /** @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 { - 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 { + 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 { + 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, + )}`; } } diff --git a/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/EntityPageOctopusDeploy.tsx b/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/EntityPageOctopusDeploy.tsx index 21080cb408..d9352a414a 100644 --- a/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/EntityPageOctopusDeploy.tsx +++ b/plugins/octopus-deploy/src/components/EntityPageOctopusDeploy/EntityPageOctopusDeploy.tsx @@ -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 ( ); }; diff --git a/plugins/octopus-deploy/src/components/ReleaseTable/ReleaseTable.tsx b/plugins/octopus-deploy/src/components/ReleaseTable/ReleaseTable.tsx index bc185546b1..b61b533667 100644 --- a/plugins/octopus-deploy/src/components/ReleaseTable/ReleaseTable.tsx +++ b/plugins/octopus-deploy/src/components/ReleaseTable/ReleaseTable.tsx @@ -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 ( - - - - Octopus Deploy - Releases ({releases ? releases.length : 0}) - - } - data={releases ?? []} - /> + +
+ + + Octopus Deploy - Releases ({releases ? releases.length : 0}) + + } + data={releases ?? []} + /> + {deepLink !== null && } + ); }; diff --git a/plugins/octopus-deploy/src/hooks/useConfig.ts b/plugins/octopus-deploy/src/hooks/useConfig.ts new file mode 100644 index 0000000000..7218bf1a2c --- /dev/null +++ b/plugins/octopus-deploy/src/hooks/useConfig.ts @@ -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, + }; +} diff --git a/plugins/octopus-deploy/src/hooks/useProject.ts b/plugins/octopus-deploy/src/hooks/useProject.ts new file mode 100644 index 0000000000..f4178fd3e0 --- /dev/null +++ b/plugins/octopus-deploy/src/hooks/useProject.ts @@ -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, + }; +} diff --git a/plugins/octopus-deploy/src/plugin.ts b/plugins/octopus-deploy/src/plugin.ts index 9365f93bdb..39b2b49fb7 100644 --- a/plugins/octopus-deploy/src/plugin.ts +++ b/plugins/octopus-deploy/src/plugin.ts @@ -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 }), }), ], });