@evergreen-dev/backstage-plugin 0.0.0-stage → 0.1.0
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/package.json +49 -4
- package/src/api.ts +52 -0
- package/src/components/LifecycleRiskCard.tsx +115 -0
- package/src/index.ts +19 -0
- package/src/plugin.ts +40 -0
- package/src/types.ts +15 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evergreen-dev/backstage-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Backstage entity card that shows Evergreen lifecycle risk per component from the portal export.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"backstage": {
|
|
9
|
+
"role": "frontend-plugin",
|
|
10
|
+
"pluginId": "evergreen",
|
|
11
|
+
"pluginPackages": [
|
|
12
|
+
"@evergreen-dev/backstage-plugin",
|
|
13
|
+
"@evergreen-dev/backstage-plugin-backend",
|
|
14
|
+
"@evergreen-dev/backstage-plugin-common"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "backstage-cli package start",
|
|
20
|
+
"build": "backstage-cli package build",
|
|
21
|
+
"lint": "backstage-cli package lint",
|
|
22
|
+
"test": "backstage-cli package test",
|
|
23
|
+
"clean": "backstage-cli package clean"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@evergreen-dev/backstage-plugin-common": "^0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@backstage/catalog-model": "^1.0.0",
|
|
30
|
+
"@backstage/core-components": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0",
|
|
31
|
+
"@backstage/core-plugin-api": "^1.0.0",
|
|
32
|
+
"@backstage/plugin-catalog-react": "^1.0.0",
|
|
33
|
+
"@material-ui/core": "^4.12.0",
|
|
34
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
35
|
+
"react-dom": "^17.0.0 || ^18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@backstage/catalog-model": "^1.7.0",
|
|
39
|
+
"@backstage/cli": "^0.30.0",
|
|
40
|
+
"@backstage/core-components": "^0.16.0",
|
|
41
|
+
"@backstage/core-plugin-api": "^1.10.0",
|
|
42
|
+
"@backstage/plugin-catalog-react": "^1.14.0",
|
|
43
|
+
"@material-ui/core": "^4.12.4",
|
|
44
|
+
"@types/react": "^18.3.0",
|
|
45
|
+
"react": "^18.3.0",
|
|
46
|
+
"react-dom": "^18.3.0"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"src"
|
|
50
|
+
]
|
|
51
|
+
}
|
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";
|
package/README.md
DELETED