Merge pull request #20751 from graemechristie/octo-plugin-link-to-project
Add link to Octopus Deploy Project from Release Table
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-octopus-deploy': patch
|
||||
---
|
||||
|
||||
Added Deep link into Octopus Deploy project from the Release Table.
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
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';
|
||||
@@ -25,6 +26,10 @@ export const OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION = 'octopus.com/project-id';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface OctopusDeployApi {
|
||||
// (undocumented)
|
||||
getConfig(): Promise<OctopusPluginConfig>;
|
||||
// (undocumented)
|
||||
getProjectInfo(projectReference: ProjectReference): Promise<OctopusProject>;
|
||||
// (undocumented)
|
||||
getReleaseProgression(opts: {
|
||||
projectReference: ProjectReference;
|
||||
@@ -38,11 +43,16 @@ export const octopusDeployApiRef: ApiRef<OctopusDeployApi>;
|
||||
// @public (undocumented)
|
||||
export class OctopusDeployClient implements OctopusDeployApi {
|
||||
constructor(options: {
|
||||
configApi: ConfigApi;
|
||||
discoveryApi: DiscoveryApi;
|
||||
fetchApi: FetchApi;
|
||||
proxyPathBase?: string;
|
||||
});
|
||||
// (undocumented)
|
||||
getConfig(): Promise<OctopusPluginConfig>;
|
||||
// (undocumented)
|
||||
getProjectInfo(projectReference: ProjectReference): Promise<OctopusProject>;
|
||||
// (undocumented)
|
||||
getReleaseProgression(opts: {
|
||||
projectReference: ProjectReference;
|
||||
releaseHistoryCount: number;
|
||||
@@ -63,12 +73,30 @@ export type OctopusEnvironment = {
|
||||
Name: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type OctopusLinks = {
|
||||
Self: string;
|
||||
Web: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type OctopusPluginConfig = {
|
||||
WebUiBaseUrl: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type OctopusProgression = {
|
||||
Environments: OctopusEnvironment[];
|
||||
Releases: OctopusReleaseProgression[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type OctopusProject = {
|
||||
Name: string;
|
||||
Slug: string;
|
||||
Links: OctopusLinks;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type OctopusRelease = {
|
||||
Id: string;
|
||||
|
||||
Vendored
+24
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -57,6 +57,8 @@
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.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<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,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-2
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user