@evergreen-dev/backstage-plugin 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # @evergreen-dev/backstage-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Backstage entity card that shows Evergreen lifecycle risk per Component. Also install `@evergreen-dev/backstage-plugin-backend`.
4
+
5
+ Setup, configuration, and the CI export step: https://github.com/arminazimi/evergreen/tree/main/integrations/backstage#readme
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@evergreen-dev/backstage-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Backstage entity card that shows Evergreen lifecycle risk per component from the portal export.",
5
+ "keywords": [
6
+ "backstage",
7
+ "backstage-plugin",
8
+ "evergreen",
9
+ "end-of-life",
10
+ "lifecycle",
11
+ "deprecation"
12
+ ],
13
+ "homepage": "https://github.com/arminazimi/evergreen/tree/main/integrations/backstage#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/arminazimi/evergreen/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/arminazimi/evergreen.git",
20
+ "directory": "integrations/backstage/plugin"
21
+ },
22
+ "license": "Apache-2.0",
23
+ "main": "src/index.ts",
24
+ "types": "src/index.ts",
25
+ "backstage": {
26
+ "role": "frontend-plugin",
27
+ "pluginId": "evergreen",
28
+ "pluginPackages": [
29
+ "@evergreen-dev/backstage-plugin",
30
+ "@evergreen-dev/backstage-plugin-backend",
31
+ "@evergreen-dev/backstage-plugin-common"
32
+ ]
33
+ },
34
+ "sideEffects": false,
35
+ "scripts": {
36
+ "start": "backstage-cli package start",
37
+ "build": "backstage-cli package build",
38
+ "lint": "backstage-cli package lint",
39
+ "test": "backstage-cli package test",
40
+ "clean": "backstage-cli package clean"
41
+ },
42
+ "dependencies": {
43
+ "@evergreen-dev/backstage-plugin-common": "^0.1.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@backstage/catalog-model": "^1.0.0",
47
+ "@backstage/core-components": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0",
48
+ "@backstage/core-plugin-api": "^1.0.0",
49
+ "@backstage/plugin-catalog-react": "^1.0.0",
50
+ "@material-ui/core": "^4.12.0",
51
+ "react": "^17.0.0 || ^18.0.0",
52
+ "react-dom": "^17.0.0 || ^18.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@backstage/catalog-model": "^1.7.0",
56
+ "@backstage/cli": "^0.30.0",
57
+ "@backstage/core-components": "^0.16.0",
58
+ "@backstage/core-plugin-api": "^1.10.0",
59
+ "@backstage/plugin-catalog-react": "^1.14.0",
60
+ "@material-ui/core": "^4.12.4",
61
+ "@types/react": "^18.3.0",
62
+ "react": "^18.3.0",
63
+ "react-dom": "^18.3.0"
64
+ },
65
+ "files": [
66
+ "src",
67
+ "README.md"
68
+ ]
69
+ }
package/src/api.ts ADDED
@@ -0,0 +1,52 @@
1
+ import {
2
+ createApiRef,
3
+ type ConfigApi,
4
+ type DiscoveryApi,
5
+ type FetchApi,
6
+ } from "@backstage/core-plugin-api";
7
+
8
+ import type { PortalCatalog, PortalService } from "@evergreen-dev/backstage-plugin-common";
9
+
10
+ export const evergreenApiRef = createApiRef<EvergreenApi>({
11
+ id: "plugin.evergreen.service",
12
+ });
13
+
14
+ export interface EvergreenApi {
15
+ getCatalog(): Promise<PortalCatalog>;
16
+ getService(servicePath: string): Promise<PortalService>;
17
+ }
18
+
19
+ export class EvergreenClient implements EvergreenApi {
20
+ private readonly discoveryApi: DiscoveryApi;
21
+ private readonly fetchApi: FetchApi;
22
+
23
+ constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi; configApi?: ConfigApi }) {
24
+ this.discoveryApi = options.discoveryApi;
25
+ this.fetchApi = options.fetchApi;
26
+ }
27
+
28
+ private async baseUrl(): Promise<string> {
29
+ return this.discoveryApi.getBaseUrl("evergreen");
30
+ }
31
+
32
+ async getCatalog(): Promise<PortalCatalog> {
33
+ const response = await this.fetchApi.fetch(`${await this.baseUrl()}/catalog`);
34
+ if (!response.ok) {
35
+ throw new Error(`Evergreen catalog request failed: HTTP ${response.status}`);
36
+ }
37
+ return (await response.json()) as PortalCatalog;
38
+ }
39
+
40
+ async getService(servicePath: string): Promise<PortalService> {
41
+ const url = new URL(`${await this.baseUrl()}/services`);
42
+ url.searchParams.set("path", servicePath);
43
+ const response = await this.fetchApi.fetch(url.toString());
44
+ if (response.status === 404) {
45
+ throw new Error(`No Evergreen portal service matches ${servicePath}`);
46
+ }
47
+ if (!response.ok) {
48
+ throw new Error(`Evergreen service request failed: HTTP ${response.status}`);
49
+ }
50
+ return (await response.json()) as PortalService;
51
+ }
52
+ }
@@ -0,0 +1,115 @@
1
+ import { useEntity } from "@backstage/plugin-catalog-react";
2
+ import {
3
+ InfoCard,
4
+ Progress,
5
+ ResponseErrorPanel,
6
+ StructuredMetadataTable,
7
+ } from "@backstage/core-components";
8
+ import { useApi } from "@backstage/core-plugin-api";
9
+ import { Link, Typography } from "@material-ui/core";
10
+ import React from "react";
11
+
12
+ import {
13
+ SERVICE_PATH_ANNOTATION,
14
+ SEVERITIES,
15
+ highestSeverity,
16
+ totalFindings,
17
+ type PortalService,
18
+ } from "@evergreen-dev/backstage-plugin-common";
19
+ import { evergreenApiRef } from "../api";
20
+
21
+ type LoadState =
22
+ | { status: "loading" }
23
+ | { status: "missing-annotation" }
24
+ | { status: "error"; error: Error }
25
+ | { status: "ready"; service: PortalService };
26
+
27
+ function useLifecycleRisk(): LoadState {
28
+ const { entity } = useEntity();
29
+ const api = useApi(evergreenApiRef);
30
+ const servicePath = entity.metadata.annotations?.[SERVICE_PATH_ANNOTATION];
31
+ const [state, setState] = React.useState<LoadState>(
32
+ servicePath ? { status: "loading" } : { status: "missing-annotation" },
33
+ );
34
+
35
+ React.useEffect(() => {
36
+ if (!servicePath) {
37
+ setState({ status: "missing-annotation" });
38
+ return undefined;
39
+ }
40
+ let cancelled = false;
41
+ setState({ status: "loading" });
42
+ api
43
+ .getService(servicePath)
44
+ .then((service) => {
45
+ if (!cancelled) setState({ status: "ready", service });
46
+ })
47
+ .catch((error: unknown) => {
48
+ if (!cancelled) {
49
+ setState({
50
+ status: "error",
51
+ error: error instanceof Error ? error : new Error(String(error)),
52
+ });
53
+ }
54
+ });
55
+ return () => {
56
+ cancelled = true;
57
+ };
58
+ }, [api, servicePath]);
59
+
60
+ return state;
61
+ }
62
+
63
+ export function LifecycleRiskCard() {
64
+ const state = useLifecycleRisk();
65
+
66
+ if (state.status === "missing-annotation") {
67
+ return (
68
+ <InfoCard title="Evergreen lifecycle risk">
69
+ <Typography variant="body2">
70
+ Add the <code>{SERVICE_PATH_ANNOTATION}</code> annotation on this
71
+ component, set to the scanned path from{" "}
72
+ <code>evergreen export --format portal</code>.
73
+ </Typography>
74
+ </InfoCard>
75
+ );
76
+ }
77
+
78
+ if (state.status === "loading") {
79
+ return (
80
+ <InfoCard title="Evergreen lifecycle risk">
81
+ <Progress />
82
+ </InfoCard>
83
+ );
84
+ }
85
+
86
+ if (state.status === "error") {
87
+ return (
88
+ <InfoCard title="Evergreen lifecycle risk">
89
+ <ResponseErrorPanel error={state.error} />
90
+ </InfoCard>
91
+ );
92
+ }
93
+
94
+ const { service } = state;
95
+ const metadata: Record<string, React.ReactNode> = {
96
+ Path: service.path,
97
+ Findings: String(totalFindings(service)),
98
+ Highest: highestSeverity(service),
99
+ "Next end of life": service.nextEndOfLife ?? "—",
100
+ Report: (
101
+ <Link href={service.reportLink} target="_blank" rel="noopener noreferrer">
102
+ Open Evergreen report
103
+ </Link>
104
+ ),
105
+ };
106
+ for (const severity of SEVERITIES) {
107
+ metadata[severity] = String(service.severities[severity]);
108
+ }
109
+
110
+ return (
111
+ <InfoCard title="Evergreen lifecycle risk" deepLink={{ title: "Report", link: service.reportLink }}>
112
+ <StructuredMetadataTable metadata={metadata} />
113
+ </InfoCard>
114
+ );
115
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export {
2
+ evergreenPlugin,
3
+ EntityEvergreenLifecycleCard,
4
+ } from "./plugin";
5
+ export { evergreenApiRef, EvergreenClient } from "./api";
6
+ export type { EvergreenApi } from "./api";
7
+ export {
8
+ PORTAL_SCHEMA_VERSION,
9
+ SERVICE_PATH_ANNOTATION,
10
+ findService,
11
+ highestSeverity,
12
+ totalFindings,
13
+ } from "@evergreen-dev/backstage-plugin-common";
14
+ export type {
15
+ PortalCatalog,
16
+ PortalService,
17
+ PortalSeverityCounts,
18
+ Severity,
19
+ } from "@evergreen-dev/backstage-plugin-common";
package/src/plugin.ts ADDED
@@ -0,0 +1,40 @@
1
+ import {
2
+ createApiFactory,
3
+ createComponentExtension,
4
+ createPlugin,
5
+ discoveryApiRef,
6
+ fetchApiRef,
7
+ configApiRef,
8
+ } from "@backstage/core-plugin-api";
9
+
10
+ import { EvergreenClient, evergreenApiRef } from "./api";
11
+
12
+ export const evergreenPlugin = createPlugin({
13
+ id: "evergreen",
14
+ apis: [
15
+ createApiFactory({
16
+ api: evergreenApiRef,
17
+ deps: {
18
+ discoveryApi: discoveryApiRef,
19
+ fetchApi: fetchApiRef,
20
+ configApi: configApiRef,
21
+ },
22
+ factory: ({ discoveryApi, fetchApi }) =>
23
+ new EvergreenClient({ discoveryApi, fetchApi }),
24
+ }),
25
+ ],
26
+ });
27
+
28
+ /**
29
+ * Entity card that shows lifecycle-risk counts, the next end-of-life date, and a report link
30
+ * for the component's `evergreen.io/service-path` annotation.
31
+ */
32
+ export const EntityEvergreenLifecycleCard = evergreenPlugin.provide(
33
+ createComponentExtension({
34
+ name: "EntityEvergreenLifecycleCard",
35
+ component: {
36
+ lazy: () =>
37
+ import("./components/LifecycleRiskCard").then((m) => m.LifecycleRiskCard),
38
+ },
39
+ }),
40
+ );
package/src/types.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type {
2
+ PortalCatalog,
3
+ PortalService,
4
+ PortalSeverityCounts,
5
+ Severity,
6
+ } from "@evergreen-dev/backstage-plugin-common";
7
+
8
+ export {
9
+ PORTAL_SCHEMA_VERSION,
10
+ SERVICE_PATH_ANNOTATION,
11
+ SEVERITIES,
12
+ findService,
13
+ highestSeverity,
14
+ totalFindings,
15
+ } from "@evergreen-dev/backstage-plugin-common";