@anyshift/backstage-plugin-anyshift 0.1.0-beta.1 → 0.1.0-beta.10
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/CHANGELOG.md +44 -0
- package/README.md +7 -1
- package/dist/api/AnyshiftApi.esm.js +21 -4
- package/dist/api/AnyshiftApi.esm.js.map +1 -1
- package/dist/components/cloud/CloudPage.esm.js +104 -0
- package/dist/components/cloud/CloudPage.esm.js.map +1 -0
- package/dist/components/cloud/CloudResourceContent.esm.js +358 -0
- package/dist/components/cloud/CloudResourceContent.esm.js.map +1 -0
- package/dist/components/entity/AnyshiftEntityContent.esm.js +42 -6
- package/dist/components/entity/AnyshiftEntityContent.esm.js.map +1 -1
- package/dist/components/entity/BriefSections.esm.js +24 -4
- package/dist/components/entity/BriefSections.esm.js.map +1 -1
- package/dist/components/entity/SectionPresenters.esm.js +122 -39
- package/dist/components/entity/SectionPresenters.esm.js.map +1 -1
- package/dist/components/explorer/AnyshiftExplorerPage.esm.js +269 -26
- package/dist/components/explorer/AnyshiftExplorerPage.esm.js.map +1 -1
- package/dist/index.d.ts +108 -7
- package/dist/index.esm.js +1 -1
- package/dist/package.json.esm.js +1 -1
- package/dist/plugin.esm.js +47 -8
- package/dist/plugin.esm.js.map +1 -1
- package/dist/routes.esm.js +2 -1
- package/dist/routes.esm.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.10
|
|
4
|
+
|
|
5
|
+
- Render why-now evidence immediately after the entity overview without claiming causality.
|
|
6
|
+
- Link affected service footprints to related Backstage catalog entities.
|
|
7
|
+
- Add estate evidence coverage, blind spots, freshness, and scoped retry to the Anyshift dashboard.
|
|
8
|
+
|
|
9
|
+
## 0.1.0-beta.9
|
|
10
|
+
|
|
11
|
+
- Render configured callers and dependencies separately from observed APM traffic.
|
|
12
|
+
- Resolve ECS configuration evidence through a dedicated runtime identity annotation.
|
|
13
|
+
|
|
14
|
+
## 0.1.0-beta.8
|
|
15
|
+
|
|
16
|
+
- Render cloud evidence timestamps with bracketed time-zone identifiers without
|
|
17
|
+
displaying `Invalid Date`.
|
|
18
|
+
- Preserve unrecognized timestamp evidence as its original value instead of
|
|
19
|
+
inventing a time.
|
|
20
|
+
|
|
21
|
+
## 0.1.0-beta.7
|
|
22
|
+
|
|
23
|
+
- Load normalized cloud changes, Terraform provenance, and drift evidence on cloud Resource pages.
|
|
24
|
+
- Resolve stable Anyshift resource identity before provider-native ARN fallback.
|
|
25
|
+
- Preserve successful evidence sections across partial upstream failures and render unknown drift neutrally.
|
|
26
|
+
|
|
27
|
+
## 0.1.0-beta.6
|
|
28
|
+
|
|
29
|
+
- Add a discoverable global Cloud inventory page.
|
|
30
|
+
- Add generic cloud Resource content with identity, ownership, relations, ARN, and console links.
|
|
31
|
+
|
|
32
|
+
## 0.1.0-beta.5
|
|
33
|
+
|
|
34
|
+
- Hide entity content for catalog resources without a supported Anyshift brief kind.
|
|
35
|
+
- Preserve the default workload view for GitHub-owned Components.
|
|
36
|
+
|
|
37
|
+
## 0.1.0-beta.3
|
|
38
|
+
|
|
39
|
+
- Show durable catalog reconciliation status on the global dashboard.
|
|
40
|
+
- Add a permission-aware **Reconcile now** operator action.
|
|
41
|
+
|
|
42
|
+
## 0.1.0-beta.2
|
|
43
|
+
|
|
44
|
+
- Coordinate the frontend package with the trusted catalog synchronization beta.
|
|
45
|
+
- Show configured, excluded, active, unclassified, and managed catalog coverage.
|
|
46
|
+
|
|
3
47
|
## 0.1.0-beta.1
|
|
4
48
|
|
|
5
49
|
- Add discoverable dashboard, navigation, API, and entity-content extensions.
|
package/README.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
New-frontend-system plugin for the Anyshift infrastructure graph. Installing the package in an app
|
|
4
4
|
created with `@backstage/frontend-defaults` and configured with `app.packages: all` discovers its
|
|
5
|
-
dashboard, navigation
|
|
5
|
+
dashboard, Cloud inventory page, navigation items, API factory, and entity content automatically.
|
|
6
|
+
|
|
7
|
+
The Cloud page lists provider-managed infrastructure Resources with searchable type, account,
|
|
8
|
+
region, and ownership metadata. Cloud Resource entities receive read-only normalized change,
|
|
9
|
+
Terraform provenance, and drift evidence alongside their stable identity, provider console link,
|
|
10
|
+
and catalog relationships. Evidence sections degrade independently, and an unknown drift verdict
|
|
11
|
+
is not presented as drift.
|
|
6
12
|
|
|
7
13
|
The package has no legacy frontend entry point. Add the corresponding
|
|
8
14
|
`@anyshift/backstage-plugin-anyshift-backend` package to the Backstage backend and configure one
|
|
@@ -27,12 +27,20 @@ class AnyshiftClient {
|
|
|
27
27
|
)}/sections/${encodeURIComponent(section)}${briefQuery(options)}`
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
|
-
async insights() {
|
|
31
|
-
return this.get(
|
|
30
|
+
async insights(options = {}) {
|
|
31
|
+
return this.get(`/insights${options.refresh ? "?refresh=1" : ""}`);
|
|
32
32
|
}
|
|
33
33
|
async datadogSignals(options = {}) {
|
|
34
34
|
return this.get(`/signals/datadog${options.refresh ? "?refresh=1" : ""}`);
|
|
35
35
|
}
|
|
36
|
+
async cloudResourceBrief(resource, options = {}) {
|
|
37
|
+
const params = new URLSearchParams({ resource });
|
|
38
|
+
if (options.since) params.set("since", options.since);
|
|
39
|
+
if (options.cursor) params.set("cursor", options.cursor);
|
|
40
|
+
if (options.diff !== void 0) params.set("diff", String(options.diff));
|
|
41
|
+
if (options.refresh) params.set("refresh", "1");
|
|
42
|
+
return this.get(`/cloud/resources/brief?${params}`);
|
|
43
|
+
}
|
|
36
44
|
async inventory(type, options = {}) {
|
|
37
45
|
const params = new URLSearchParams({ type });
|
|
38
46
|
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
@@ -47,11 +55,14 @@ class AnyshiftClient {
|
|
|
47
55
|
body: JSON.stringify({ sql })
|
|
48
56
|
});
|
|
49
57
|
}
|
|
58
|
+
async reconcileCatalog() {
|
|
59
|
+
return this.request("/anyshift/reconcile", { method: "POST" }, "catalog");
|
|
60
|
+
}
|
|
50
61
|
async get(path) {
|
|
51
62
|
return this.request(path, { method: "GET" });
|
|
52
63
|
}
|
|
53
|
-
async request(path, init) {
|
|
54
|
-
const baseUrl = await this.discoveryApi.getBaseUrl(
|
|
64
|
+
async request(path, init, pluginId = "anyshift") {
|
|
65
|
+
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
|
|
55
66
|
const response = await this.fetchApi.fetch(`${baseUrl}${path}`, init);
|
|
56
67
|
const text = await response.text();
|
|
57
68
|
let payload;
|
|
@@ -72,6 +83,12 @@ function briefQuery(options) {
|
|
|
72
83
|
const params = new URLSearchParams();
|
|
73
84
|
if (options.namespace) params.set("namespace", options.namespace);
|
|
74
85
|
if (options.refresh) params.set("refresh", "1");
|
|
86
|
+
if (options.configurationEndpoints)
|
|
87
|
+
params.set("configurationEndpoints", options.configurationEndpoints);
|
|
88
|
+
if (options.configurationDependencies)
|
|
89
|
+
params.set("configurationDependencies", options.configurationDependencies);
|
|
90
|
+
if (options.configurationTarget)
|
|
91
|
+
params.set("configurationTarget", options.configurationTarget);
|
|
75
92
|
const query = params.toString();
|
|
76
93
|
return query ? `?${query}` : "";
|
|
77
94
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnyshiftApi.esm.js","sources":["../../src/api/AnyshiftApi.ts"],"sourcesContent":["import {\n createApiRef,\n type DiscoveryApi,\n type FetchApi,\n} from '@backstage/frontend-plugin-api';\nimport type {\n BriefSectionName,\n BriefSectionResponse,\n AnyshiftCapabilities,\n CatalogSummary,\n DatadogSignals,\n EstateInsights,\n QueryResult,\n ServiceBrief,\n} from './types';\n\nexport interface AnyshiftApi {\n capabilities(): Promise<AnyshiftCapabilities>;\n brief(\n kind: string,\n target: string,\n options?: BriefRequestOptions,\n ): Promise<ServiceBrief>;\n briefSection(\n kind: string,\n target: string,\n section: BriefSectionName,\n options?: BriefRequestOptions,\n ): Promise<BriefSectionResponse>;\n insights(): Promise<EstateInsights>;\n datadogSignals(options?: { refresh?: boolean }): Promise<DatadogSignals>;\n inventory(\n type: string,\n options?: { limit?: number; offset?: number },\n ): Promise<CatalogSummary>;\n query(sql: string): Promise<QueryResult>;\n}\n\nexport interface BriefRequestOptions {\n namespace?: string;\n refresh?: boolean;\n}\n\nexport const anyshiftApiRef = createApiRef<AnyshiftApi>({\n id: 'plugin.anyshift.service',\n});\n\nexport class AnyshiftClient implements AnyshiftApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly fetchApi: FetchApi;\n\n constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) {\n this.discoveryApi = options.discoveryApi;\n this.fetchApi = options.fetchApi;\n }\n\n async capabilities(): Promise<AnyshiftCapabilities> {\n return this.get('/capabilities');\n }\n\n async brief(\n kind: string,\n target: string,\n options: BriefRequestOptions = {},\n ): Promise<ServiceBrief> {\n return this.get(\n `/brief/${encodeURIComponent(kind)}/${encodeURIComponent(\n target,\n )}${briefQuery(options)}`,\n );\n }\n\n async briefSection(\n kind: string,\n target: string,\n section: BriefSectionName,\n options: BriefRequestOptions = {},\n ): Promise<BriefSectionResponse> {\n return this.get(\n `/brief/${encodeURIComponent(kind)}/${encodeURIComponent(\n target,\n )}/sections/${encodeURIComponent(section)}${briefQuery(options)}`,\n );\n }\n\n async insights(): Promise<EstateInsights> {\n return this.get(
|
|
1
|
+
{"version":3,"file":"AnyshiftApi.esm.js","sources":["../../src/api/AnyshiftApi.ts"],"sourcesContent":["import {\n createApiRef,\n type DiscoveryApi,\n type FetchApi,\n} from '@backstage/frontend-plugin-api';\nimport type {\n BriefSectionName,\n BriefSectionResponse,\n AnyshiftCapabilities,\n CatalogSummary,\n CatalogSyncReport,\n CloudResourceBrief,\n DatadogSignals,\n EstateInsights,\n QueryResult,\n ServiceBrief,\n} from './types';\n\nexport interface AnyshiftApi {\n capabilities(): Promise<AnyshiftCapabilities>;\n brief(\n kind: string,\n target: string,\n options?: BriefRequestOptions,\n ): Promise<ServiceBrief>;\n briefSection(\n kind: string,\n target: string,\n section: BriefSectionName,\n options?: BriefRequestOptions,\n ): Promise<BriefSectionResponse>;\n insights(options?: { refresh?: boolean }): Promise<EstateInsights>;\n datadogSignals(options?: { refresh?: boolean }): Promise<DatadogSignals>;\n cloudResourceBrief(\n resource: string,\n options?: CloudResourceBriefRequestOptions,\n ): Promise<CloudResourceBrief>;\n inventory(\n type: string,\n options?: { limit?: number; offset?: number },\n ): Promise<CatalogSummary>;\n query(sql: string): Promise<QueryResult>;\n reconcileCatalog(): Promise<CatalogSyncAccepted>;\n}\n\nexport interface BriefRequestOptions {\n namespace?: string;\n refresh?: boolean;\n configurationEndpoints?: string;\n configurationDependencies?: string;\n configurationTarget?: string;\n}\n\nexport interface CloudResourceBriefRequestOptions {\n since?: string;\n cursor?: string;\n diff?: boolean;\n refresh?: boolean;\n}\n\nexport interface CatalogSyncAccepted {\n accepted: true;\n acceptedAt: string;\n previous?: CatalogSyncReport;\n}\n\nexport const anyshiftApiRef = createApiRef<AnyshiftApi>({\n id: 'plugin.anyshift.service',\n});\n\nexport class AnyshiftClient implements AnyshiftApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly fetchApi: FetchApi;\n\n constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) {\n this.discoveryApi = options.discoveryApi;\n this.fetchApi = options.fetchApi;\n }\n\n async capabilities(): Promise<AnyshiftCapabilities> {\n return this.get('/capabilities');\n }\n\n async brief(\n kind: string,\n target: string,\n options: BriefRequestOptions = {},\n ): Promise<ServiceBrief> {\n return this.get(\n `/brief/${encodeURIComponent(kind)}/${encodeURIComponent(\n target,\n )}${briefQuery(options)}`,\n );\n }\n\n async briefSection(\n kind: string,\n target: string,\n section: BriefSectionName,\n options: BriefRequestOptions = {},\n ): Promise<BriefSectionResponse> {\n return this.get(\n `/brief/${encodeURIComponent(kind)}/${encodeURIComponent(\n target,\n )}/sections/${encodeURIComponent(section)}${briefQuery(options)}`,\n );\n }\n\n async insights(options: { refresh?: boolean } = {}): Promise<EstateInsights> {\n return this.get(`/insights${options.refresh ? '?refresh=1' : ''}`);\n }\n\n async datadogSignals(\n options: { refresh?: boolean } = {},\n ): Promise<DatadogSignals> {\n return this.get(`/signals/datadog${options.refresh ? '?refresh=1' : ''}`);\n }\n\n async cloudResourceBrief(\n resource: string,\n options: CloudResourceBriefRequestOptions = {},\n ): Promise<CloudResourceBrief> {\n const params = new URLSearchParams({ resource });\n if (options.since) params.set('since', options.since);\n if (options.cursor) params.set('cursor', options.cursor);\n if (options.diff !== undefined) params.set('diff', String(options.diff));\n if (options.refresh) params.set('refresh', '1');\n return this.get(`/cloud/resources/brief?${params}`);\n }\n\n async inventory(\n type: string,\n options: { limit?: number; offset?: number } = {},\n ): Promise<CatalogSummary> {\n const params = new URLSearchParams({ type });\n if (options.limit !== undefined) params.set('limit', String(options.limit));\n if (options.offset !== undefined)\n params.set('offset', String(options.offset));\n return this.get(`/inventory?${params}`);\n }\n\n async query(sql: string): Promise<QueryResult> {\n return this.request('/query', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sql }),\n });\n }\n\n async reconcileCatalog(): Promise<CatalogSyncAccepted> {\n return this.request('/anyshift/reconcile', { method: 'POST' }, 'catalog');\n }\n\n private async get<T>(path: string): Promise<T> {\n return this.request(path, { method: 'GET' });\n }\n\n private async request<T>(\n path: string,\n init: RequestInit,\n pluginId = 'anyshift',\n ): Promise<T> {\n const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);\n const response = await this.fetchApi.fetch(`${baseUrl}${path}`, init);\n const text = await response.text();\n let payload: any;\n try {\n payload = text ? JSON.parse(text) : {};\n } catch {\n payload = {};\n }\n if (!response.ok) {\n throw new Error(\n payload?.error?.message ||\n `Anyshift request failed (${response.status})`,\n );\n }\n return payload as T;\n }\n}\n\nfunction briefQuery(options: BriefRequestOptions): string {\n const params = new URLSearchParams();\n if (options.namespace) params.set('namespace', options.namespace);\n if (options.refresh) params.set('refresh', '1');\n if (options.configurationEndpoints)\n params.set('configurationEndpoints', options.configurationEndpoints);\n if (options.configurationDependencies)\n params.set('configurationDependencies', options.configurationDependencies);\n if (options.configurationTarget)\n params.set('configurationTarget', options.configurationTarget);\n const query = params.toString();\n return query ? `?${query}` : '';\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { Typography, Grid } from '@material-ui/core';
|
|
4
|
+
import { Page, Header, Content, ContentHeader, Progress, InfoCard, Table } from '@backstage/core-components';
|
|
5
|
+
import { ANYSHIFT_CLOUD_ACCOUNT_ANNOTATION, ANYSHIFT_CLOUD_REGION_ANNOTATION, ANYSHIFT_RESOURCE_TYPE_ANNOTATION, ANYSHIFT_CLOUD_PROVIDER_ANNOTATION } from '@anyshift/backstage-plugin-anyshift-common';
|
|
6
|
+
import { catalogApiRef, EntityRefLink } from '@backstage/plugin-catalog-react';
|
|
7
|
+
import { useApi } from '@backstage/frontend-plugin-api';
|
|
8
|
+
|
|
9
|
+
function isCloudResource(entity) {
|
|
10
|
+
return entity.kind.toLocaleLowerCase("en-US") === "resource" && Boolean(entity.metadata.annotations?.[ANYSHIFT_CLOUD_PROVIDER_ANNOTATION]);
|
|
11
|
+
}
|
|
12
|
+
function cloudResourceRows(entities) {
|
|
13
|
+
return entities.filter(isCloudResource).map((entity) => {
|
|
14
|
+
const annotations = entity.metadata.annotations ?? {};
|
|
15
|
+
return {
|
|
16
|
+
entity,
|
|
17
|
+
name: entity.metadata.title || entity.metadata.name,
|
|
18
|
+
provider: annotations[ANYSHIFT_CLOUD_PROVIDER_ANNOTATION]?.toUpperCase() || "Unknown",
|
|
19
|
+
resourceType: annotations[ANYSHIFT_RESOURCE_TYPE_ANNOTATION] || String(entity.spec?.type || "resource"),
|
|
20
|
+
region: annotations[ANYSHIFT_CLOUD_REGION_ANNOTATION] || "Global",
|
|
21
|
+
account: annotations[ANYSHIFT_CLOUD_ACCOUNT_ANNOTATION] || "Not available",
|
|
22
|
+
owner: typeof entity.spec?.owner === "string" ? entity.spec.owner : "Not assigned"
|
|
23
|
+
};
|
|
24
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
25
|
+
}
|
|
26
|
+
const columns = [
|
|
27
|
+
{
|
|
28
|
+
title: "Resource",
|
|
29
|
+
field: "name",
|
|
30
|
+
render: (row) => /* @__PURE__ */ jsx(EntityRefLink, { entityRef: row.entity, title: row.name })
|
|
31
|
+
},
|
|
32
|
+
{ title: "Type", field: "resourceType" },
|
|
33
|
+
{ title: "Provider", field: "provider" },
|
|
34
|
+
{ title: "Region", field: "region" },
|
|
35
|
+
{ title: "Account", field: "account" },
|
|
36
|
+
{ title: "Owner", field: "owner" }
|
|
37
|
+
];
|
|
38
|
+
const CloudPage = () => {
|
|
39
|
+
const catalogApi = useApi(catalogApiRef);
|
|
40
|
+
const [entities, setEntities] = useState();
|
|
41
|
+
const [error, setError] = useState();
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
catalogApi.getEntities({ filter: { kind: "Resource" } }).then((response) => setEntities(response.items)).catch(
|
|
44
|
+
(cause) => setError(cause instanceof Error ? cause.message : String(cause))
|
|
45
|
+
);
|
|
46
|
+
}, [catalogApi]);
|
|
47
|
+
const rows = useMemo(() => cloudResourceRows(entities ?? []), [entities]);
|
|
48
|
+
const summary = useMemo(
|
|
49
|
+
() => ({
|
|
50
|
+
resources: rows.length,
|
|
51
|
+
types: new Set(rows.map((row) => row.resourceType)).size,
|
|
52
|
+
accounts: new Set(
|
|
53
|
+
rows.map((row) => row.account).filter((account) => account !== "Not available")
|
|
54
|
+
).size,
|
|
55
|
+
regions: new Set(
|
|
56
|
+
rows.map((row) => row.region).filter((region) => region !== "Global")
|
|
57
|
+
).size
|
|
58
|
+
}),
|
|
59
|
+
[rows]
|
|
60
|
+
);
|
|
61
|
+
return /* @__PURE__ */ jsxs(Page, { themeId: "tool", children: [
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
Header,
|
|
64
|
+
{
|
|
65
|
+
title: "Cloud",
|
|
66
|
+
subtitle: "Read-only cloud inventory synchronized through Anyshift"
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
/* @__PURE__ */ jsxs(Content, { children: [
|
|
70
|
+
/* @__PURE__ */ jsx(ContentHeader, { title: "Cloud inventory" }),
|
|
71
|
+
error ? /* @__PURE__ */ jsx(Typography, { color: "error", children: error }) : !entities ? /* @__PURE__ */ jsx(Progress, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
72
|
+
/* @__PURE__ */ jsx(InfoCard, { children: /* @__PURE__ */ jsxs(Grid, { container: true, spacing: 2, children: [
|
|
73
|
+
/* @__PURE__ */ jsx(Summary, { label: "Resources", value: summary.resources }),
|
|
74
|
+
/* @__PURE__ */ jsx(Summary, { label: "Types", value: summary.types }),
|
|
75
|
+
/* @__PURE__ */ jsx(Summary, { label: "Accounts", value: summary.accounts }),
|
|
76
|
+
/* @__PURE__ */ jsx(Summary, { label: "Regions", value: summary.regions })
|
|
77
|
+
] }) }),
|
|
78
|
+
/* @__PURE__ */ jsx(
|
|
79
|
+
Table,
|
|
80
|
+
{
|
|
81
|
+
title: "Cloud resources",
|
|
82
|
+
columns,
|
|
83
|
+
data: rows,
|
|
84
|
+
options: {
|
|
85
|
+
filtering: true,
|
|
86
|
+
paging: true,
|
|
87
|
+
pageSize: 20,
|
|
88
|
+
pageSizeOptions: [20, 50, 100],
|
|
89
|
+
search: true
|
|
90
|
+
},
|
|
91
|
+
emptyContent: /* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: "No cloud resources have been synchronized yet." })
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
] })
|
|
95
|
+
] })
|
|
96
|
+
] });
|
|
97
|
+
};
|
|
98
|
+
const Summary = ({ label, value }) => /* @__PURE__ */ jsxs(Grid, { item: true, xs: 6, md: 3, children: [
|
|
99
|
+
/* @__PURE__ */ jsx(Typography, { variant: "h5", children: value }),
|
|
100
|
+
/* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: label })
|
|
101
|
+
] });
|
|
102
|
+
|
|
103
|
+
export { CloudPage, cloudResourceRows, isCloudResource };
|
|
104
|
+
//# sourceMappingURL=CloudPage.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloudPage.esm.js","sources":["../../../src/components/cloud/CloudPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Grid, Typography } from '@material-ui/core';\nimport {\n Content,\n ContentHeader,\n Header,\n InfoCard,\n Page,\n Progress,\n Table,\n type TableColumn,\n} from '@backstage/core-components';\nimport type { Entity } from '@backstage/catalog-model';\nimport {\n ANYSHIFT_CLOUD_ACCOUNT_ANNOTATION,\n ANYSHIFT_CLOUD_PROVIDER_ANNOTATION,\n ANYSHIFT_CLOUD_REGION_ANNOTATION,\n ANYSHIFT_RESOURCE_TYPE_ANNOTATION,\n} from '@anyshift/backstage-plugin-anyshift-common';\nimport { catalogApiRef, EntityRefLink } from '@backstage/plugin-catalog-react';\nimport { useApi } from '@backstage/frontend-plugin-api';\n\nexport interface CloudResourceRow {\n entity: Entity;\n name: string;\n provider: string;\n resourceType: string;\n region: string;\n account: string;\n owner: string;\n}\n\nexport function isCloudResource(entity: Entity): boolean {\n return (\n entity.kind.toLocaleLowerCase('en-US') === 'resource' &&\n Boolean(entity.metadata.annotations?.[ANYSHIFT_CLOUD_PROVIDER_ANNOTATION])\n );\n}\n\nexport function cloudResourceRows(entities: Entity[]): CloudResourceRow[] {\n return entities\n .filter(isCloudResource)\n .map(entity => {\n const annotations = entity.metadata.annotations ?? {};\n return {\n entity,\n name: entity.metadata.title || entity.metadata.name,\n provider:\n annotations[ANYSHIFT_CLOUD_PROVIDER_ANNOTATION]?.toUpperCase() ||\n 'Unknown',\n resourceType:\n annotations[ANYSHIFT_RESOURCE_TYPE_ANNOTATION] ||\n String(entity.spec?.type || 'resource'),\n region: annotations[ANYSHIFT_CLOUD_REGION_ANNOTATION] || 'Global',\n account:\n annotations[ANYSHIFT_CLOUD_ACCOUNT_ANNOTATION] || 'Not available',\n owner:\n typeof entity.spec?.owner === 'string'\n ? entity.spec.owner\n : 'Not assigned',\n };\n })\n .sort((a, b) => a.name.localeCompare(b.name));\n}\n\nconst columns: TableColumn<CloudResourceRow>[] = [\n {\n title: 'Resource',\n field: 'name',\n render: row => <EntityRefLink entityRef={row.entity} title={row.name} />,\n },\n { title: 'Type', field: 'resourceType' },\n { title: 'Provider', field: 'provider' },\n { title: 'Region', field: 'region' },\n { title: 'Account', field: 'account' },\n { title: 'Owner', field: 'owner' },\n];\n\nexport const CloudPage = () => {\n const catalogApi = useApi(catalogApiRef);\n const [entities, setEntities] = useState<Entity[]>();\n const [error, setError] = useState<string>();\n\n useEffect(() => {\n catalogApi\n .getEntities({ filter: { kind: 'Resource' } })\n .then(response => setEntities(response.items))\n .catch(cause =>\n setError(cause instanceof Error ? cause.message : String(cause)),\n );\n }, [catalogApi]);\n\n const rows = useMemo(() => cloudResourceRows(entities ?? []), [entities]);\n const summary = useMemo(\n () => ({\n resources: rows.length,\n types: new Set(rows.map(row => row.resourceType)).size,\n accounts: new Set(\n rows\n .map(row => row.account)\n .filter(account => account !== 'Not available'),\n ).size,\n regions: new Set(\n rows.map(row => row.region).filter(region => region !== 'Global'),\n ).size,\n }),\n [rows],\n );\n\n return (\n <Page themeId=\"tool\">\n <Header\n title=\"Cloud\"\n subtitle=\"Read-only cloud inventory synchronized through Anyshift\"\n />\n <Content>\n <ContentHeader title=\"Cloud inventory\" />\n {error ? (\n <Typography color=\"error\">{error}</Typography>\n ) : !entities ? (\n <Progress />\n ) : (\n <>\n <InfoCard>\n <Grid container spacing={2}>\n <Summary label=\"Resources\" value={summary.resources} />\n <Summary label=\"Types\" value={summary.types} />\n <Summary label=\"Accounts\" value={summary.accounts} />\n <Summary label=\"Regions\" value={summary.regions} />\n </Grid>\n </InfoCard>\n <Table\n title=\"Cloud resources\"\n columns={columns}\n data={rows}\n options={{\n filtering: true,\n paging: true,\n pageSize: 20,\n pageSizeOptions: [20, 50, 100],\n search: true,\n }}\n emptyContent={\n <Typography color=\"textSecondary\">\n No cloud resources have been synchronized yet.\n </Typography>\n }\n />\n </>\n )}\n </Content>\n </Page>\n );\n};\n\nconst Summary = ({ label, value }: { label: string; value: number }) => (\n <Grid item xs={6} md={3}>\n <Typography variant=\"h5\">{value}</Typography>\n <Typography color=\"textSecondary\">{label}</Typography>\n </Grid>\n);\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
3
|
+
import { Button, Grid, Typography, CircularProgress, Chip } from '@material-ui/core';
|
|
4
|
+
import { Alert } from '@material-ui/lab';
|
|
5
|
+
import { Progress, InfoCard, Link } from '@backstage/core-components';
|
|
6
|
+
import { useApi } from '@backstage/frontend-plugin-api';
|
|
7
|
+
import { ANYSHIFT_RESOURCE_ID_ANNOTATION, ANYSHIFT_CLOUD_RESOURCE_ARN_ANNOTATION, ANYSHIFT_CLOUD_PROVIDER_ANNOTATION, ANYSHIFT_RESOURCE_TYPE_ANNOTATION, ANYSHIFT_CLOUD_REGION_ANNOTATION, ANYSHIFT_CLOUD_ACCOUNT_ANNOTATION } from '@anyshift/backstage-plugin-anyshift-common';
|
|
8
|
+
import { useEntity, EntityRefLink } from '@backstage/plugin-catalog-react';
|
|
9
|
+
import { anyshiftApiRef } from '../../api/AnyshiftApi.esm.js';
|
|
10
|
+
|
|
11
|
+
function cloudResourceIdentity(entity) {
|
|
12
|
+
const annotations = entity.metadata.annotations ?? {};
|
|
13
|
+
return annotations[ANYSHIFT_RESOURCE_ID_ANNOTATION] || annotations[ANYSHIFT_CLOUD_RESOURCE_ARN_ANNOTATION];
|
|
14
|
+
}
|
|
15
|
+
const CloudResourceContent = () => {
|
|
16
|
+
const { entity } = useEntity();
|
|
17
|
+
const api = useApi(anyshiftApiRef);
|
|
18
|
+
const resourceId = cloudResourceIdentity(entity);
|
|
19
|
+
const [brief, setBrief] = useState();
|
|
20
|
+
const [error, setError] = useState();
|
|
21
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
22
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
23
|
+
const load = useCallback(
|
|
24
|
+
async (refresh = false) => {
|
|
25
|
+
if (!resourceId) return;
|
|
26
|
+
setRefreshing(refresh);
|
|
27
|
+
setError(void 0);
|
|
28
|
+
try {
|
|
29
|
+
setBrief(
|
|
30
|
+
await api.cloudResourceBrief(resourceId, {
|
|
31
|
+
since: "24h",
|
|
32
|
+
diff: true,
|
|
33
|
+
refresh
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
} catch (loadError) {
|
|
37
|
+
setError(
|
|
38
|
+
loadError instanceof Error ? loadError.message : "Unable to load cloud evidence"
|
|
39
|
+
);
|
|
40
|
+
} finally {
|
|
41
|
+
setRefreshing(false);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
[api, resourceId]
|
|
45
|
+
);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
void load();
|
|
48
|
+
}, [load]);
|
|
49
|
+
const loadMore = useCallback(async () => {
|
|
50
|
+
const cursor = brief?.changes.data?.page.nextCursor;
|
|
51
|
+
if (!resourceId || !cursor) return;
|
|
52
|
+
setLoadingMore(true);
|
|
53
|
+
try {
|
|
54
|
+
const next = await api.cloudResourceBrief(resourceId, {
|
|
55
|
+
since: brief.since,
|
|
56
|
+
diff: true,
|
|
57
|
+
cursor
|
|
58
|
+
});
|
|
59
|
+
setBrief((current) => {
|
|
60
|
+
if (!current?.changes.data || next.changes.state !== "data" || !next.changes.data) {
|
|
61
|
+
return current;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
...current,
|
|
65
|
+
refreshedAt: next.refreshedAt,
|
|
66
|
+
changes: {
|
|
67
|
+
state: "data",
|
|
68
|
+
data: {
|
|
69
|
+
...next.changes.data,
|
|
70
|
+
items: [
|
|
71
|
+
...current.changes.data.items,
|
|
72
|
+
...next.changes.data.items
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
} catch (loadError) {
|
|
79
|
+
setError(
|
|
80
|
+
loadError instanceof Error ? loadError.message : "Unable to load more cloud changes"
|
|
81
|
+
);
|
|
82
|
+
} finally {
|
|
83
|
+
setLoadingMore(false);
|
|
84
|
+
}
|
|
85
|
+
}, [api, brief, resourceId]);
|
|
86
|
+
if (!resourceId) {
|
|
87
|
+
return /* @__PURE__ */ jsx(Alert, { severity: "info", children: "This catalog entity has no stable Anyshift resource ID or cloud ARN." });
|
|
88
|
+
}
|
|
89
|
+
if (error && !brief) {
|
|
90
|
+
return /* @__PURE__ */ jsxs(Alert, { severity: "error", children: [
|
|
91
|
+
error,
|
|
92
|
+
" ",
|
|
93
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => void load(true), children: "Retry" })
|
|
94
|
+
] });
|
|
95
|
+
}
|
|
96
|
+
if (!brief) return /* @__PURE__ */ jsx(Progress, {});
|
|
97
|
+
return /* @__PURE__ */ jsx(
|
|
98
|
+
CloudResourceBriefView,
|
|
99
|
+
{
|
|
100
|
+
entity,
|
|
101
|
+
brief,
|
|
102
|
+
refreshing,
|
|
103
|
+
onRefresh: () => void load(true),
|
|
104
|
+
loadingMore,
|
|
105
|
+
onLoadMore: () => void loadMore(),
|
|
106
|
+
error
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
const CloudResourceBriefView = ({
|
|
111
|
+
entity,
|
|
112
|
+
brief,
|
|
113
|
+
refreshing,
|
|
114
|
+
onRefresh,
|
|
115
|
+
loadingMore,
|
|
116
|
+
onLoadMore,
|
|
117
|
+
error
|
|
118
|
+
}) => {
|
|
119
|
+
const annotations = entity.metadata.annotations ?? {};
|
|
120
|
+
const relationships = (entity.relations ?? []).filter(
|
|
121
|
+
(relation) => ["dependencyOf", "dependsOn"].includes(relation.type)
|
|
122
|
+
);
|
|
123
|
+
const consoleLink = entity.metadata.links?.find(
|
|
124
|
+
(link) => link.url.startsWith("https://console.aws.amazon.com/")
|
|
125
|
+
);
|
|
126
|
+
return /* @__PURE__ */ jsxs(Grid, { container: true, spacing: 3, children: [
|
|
127
|
+
error && /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(Alert, { severity: "error", children: error }) }),
|
|
128
|
+
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(
|
|
129
|
+
InfoCard,
|
|
130
|
+
{
|
|
131
|
+
title: "Cloud evidence",
|
|
132
|
+
action: /* @__PURE__ */ jsx(
|
|
133
|
+
Button,
|
|
134
|
+
{
|
|
135
|
+
size: "small",
|
|
136
|
+
onClick: onRefresh,
|
|
137
|
+
disabled: refreshing,
|
|
138
|
+
startIcon: refreshing ? /* @__PURE__ */ jsx(CircularProgress, { size: 14 }) : void 0,
|
|
139
|
+
children: "Refresh"
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
children: /* @__PURE__ */ jsxs(Typography, { color: "textSecondary", children: [
|
|
143
|
+
"Read-only Anyshift evidence \xB7 last refreshed",
|
|
144
|
+
" ",
|
|
145
|
+
formatCloudTimestamp(brief.refreshedAt),
|
|
146
|
+
" \xB7 window ",
|
|
147
|
+
brief.since
|
|
148
|
+
] })
|
|
149
|
+
}
|
|
150
|
+
) }),
|
|
151
|
+
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(
|
|
152
|
+
ChangesSection,
|
|
153
|
+
{
|
|
154
|
+
section: brief.changes,
|
|
155
|
+
loadingMore,
|
|
156
|
+
onLoadMore
|
|
157
|
+
}
|
|
158
|
+
) }),
|
|
159
|
+
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, md: 6, children: /* @__PURE__ */ jsx(ProvenanceSection, { section: brief.provenance }) }),
|
|
160
|
+
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, md: 6, children: /* @__PURE__ */ jsx(DriftSection, { section: brief.drift }) }),
|
|
161
|
+
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, md: 6, children: /* @__PURE__ */ jsxs(InfoCard, { title: "Cloud identity", children: [
|
|
162
|
+
/* @__PURE__ */ jsx(
|
|
163
|
+
Definition,
|
|
164
|
+
{
|
|
165
|
+
label: "Provider",
|
|
166
|
+
value: annotations[ANYSHIFT_CLOUD_PROVIDER_ANNOTATION]
|
|
167
|
+
}
|
|
168
|
+
),
|
|
169
|
+
/* @__PURE__ */ jsx(
|
|
170
|
+
Definition,
|
|
171
|
+
{
|
|
172
|
+
label: "Resource type",
|
|
173
|
+
value: annotations[ANYSHIFT_RESOURCE_TYPE_ANNOTATION] || String(entity.spec?.type || "")
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
/* @__PURE__ */ jsx(
|
|
177
|
+
Definition,
|
|
178
|
+
{
|
|
179
|
+
label: "Region",
|
|
180
|
+
value: annotations[ANYSHIFT_CLOUD_REGION_ANNOTATION] || "Global"
|
|
181
|
+
}
|
|
182
|
+
),
|
|
183
|
+
/* @__PURE__ */ jsx(
|
|
184
|
+
Definition,
|
|
185
|
+
{
|
|
186
|
+
label: "Account",
|
|
187
|
+
value: annotations[ANYSHIFT_CLOUD_ACCOUNT_ANNOTATION]
|
|
188
|
+
}
|
|
189
|
+
),
|
|
190
|
+
consoleLink && /* @__PURE__ */ jsx(Link, { to: consoleLink.url, children: "Open in AWS Console" })
|
|
191
|
+
] }) }),
|
|
192
|
+
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, md: 6, children: /* @__PURE__ */ jsxs(InfoCard, { title: "Catalog context", children: [
|
|
193
|
+
/* @__PURE__ */ jsx(
|
|
194
|
+
Definition,
|
|
195
|
+
{
|
|
196
|
+
label: "Owner",
|
|
197
|
+
value: typeof entity.spec?.owner === "string" ? entity.spec.owner : void 0
|
|
198
|
+
}
|
|
199
|
+
),
|
|
200
|
+
/* @__PURE__ */ jsx(
|
|
201
|
+
Definition,
|
|
202
|
+
{
|
|
203
|
+
label: "Namespace",
|
|
204
|
+
value: entity.metadata.namespace || "default"
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
/* @__PURE__ */ jsx(Typography, { variant: "subtitle2", children: "Relationships" }),
|
|
208
|
+
relationships.length === 0 ? /* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: "No component or resource relationships are available yet." }) : /* @__PURE__ */ jsx("ul", { children: relationships.map((relation) => /* @__PURE__ */ jsxs("li", { children: [
|
|
209
|
+
/* @__PURE__ */ jsx(
|
|
210
|
+
Chip,
|
|
211
|
+
{
|
|
212
|
+
size: "small",
|
|
213
|
+
label: relation.type,
|
|
214
|
+
style: { marginRight: 8 }
|
|
215
|
+
}
|
|
216
|
+
),
|
|
217
|
+
/* @__PURE__ */ jsx(EntityRefLink, { entityRef: relation.targetRef })
|
|
218
|
+
] }, `${relation.type}:${relation.targetRef}`)) })
|
|
219
|
+
] }) }),
|
|
220
|
+
/* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(InfoCard, { title: "Stable resource identity", children: /* @__PURE__ */ jsx(
|
|
221
|
+
Typography,
|
|
222
|
+
{
|
|
223
|
+
component: "code",
|
|
224
|
+
style: { overflowWrap: "anywhere", whiteSpace: "pre-wrap" },
|
|
225
|
+
children: brief.resourceId
|
|
226
|
+
}
|
|
227
|
+
) }) })
|
|
228
|
+
] });
|
|
229
|
+
};
|
|
230
|
+
const ChangesSection = ({
|
|
231
|
+
section,
|
|
232
|
+
loadingMore,
|
|
233
|
+
onLoadMore
|
|
234
|
+
}) => /* @__PURE__ */ jsx(InfoCard, { title: "Recent cloud changes", children: /* @__PURE__ */ jsxs(SectionState, { section, children: [
|
|
235
|
+
section.data?.warnings?.map((warning) => /* @__PURE__ */ jsx(Alert, { severity: "warning", style: { marginBottom: 8 }, children: humanize(warning) }, warning)),
|
|
236
|
+
section.data?.items?.length ? section.data.items.map((event) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: 20 }, children: [
|
|
237
|
+
/* @__PURE__ */ jsx(Typography, { variant: "subtitle1", children: event.summary || humanize(event.type) }),
|
|
238
|
+
/* @__PURE__ */ jsxs(Typography, { color: "textSecondary", children: [
|
|
239
|
+
formatCloudTimestamp(event.timestamp),
|
|
240
|
+
" \xB7 ",
|
|
241
|
+
event.category,
|
|
242
|
+
" \xB7",
|
|
243
|
+
" ",
|
|
244
|
+
event.evidence.source,
|
|
245
|
+
" \xB7 ",
|
|
246
|
+
event.evidence.status,
|
|
247
|
+
event.actor?.name || event.actor?.id ? ` \xB7 ${event.actor.name || event.actor.id}` : ""
|
|
248
|
+
] }),
|
|
249
|
+
event.change.fields.length > 0 && /* @__PURE__ */ jsxs("details", { children: [
|
|
250
|
+
/* @__PURE__ */ jsx("summary", { children: "View sanitized field changes" }),
|
|
251
|
+
/* @__PURE__ */ jsx("ul", { children: event.change.fields.map((field) => {
|
|
252
|
+
const detail = event.change.details.find(
|
|
253
|
+
(candidate) => candidate.field === field
|
|
254
|
+
);
|
|
255
|
+
return /* @__PURE__ */ jsxs("li", { children: [
|
|
256
|
+
/* @__PURE__ */ jsx("code", { children: field }),
|
|
257
|
+
detail ? `: ${detail.before ?? "\u2205"} \u2192 ${detail.after ?? "\u2205"}` : ""
|
|
258
|
+
] }, field);
|
|
259
|
+
}) })
|
|
260
|
+
] })
|
|
261
|
+
] }, event.id)) : /* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: "No cloud changes were found in this window." }),
|
|
262
|
+
section.data?.page.hasMore && onLoadMore && /* @__PURE__ */ jsx(
|
|
263
|
+
Button,
|
|
264
|
+
{
|
|
265
|
+
size: "small",
|
|
266
|
+
onClick: onLoadMore,
|
|
267
|
+
disabled: loadingMore,
|
|
268
|
+
startIcon: loadingMore ? /* @__PURE__ */ jsx(CircularProgress, { size: 14 }) : void 0,
|
|
269
|
+
children: "Load more changes"
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
] }) });
|
|
273
|
+
const ProvenanceSection = ({
|
|
274
|
+
section
|
|
275
|
+
}) => /* @__PURE__ */ jsx(InfoCard, { title: "IaC provenance", children: /* @__PURE__ */ jsx(SectionState, { section, children: section.data?.resources?.length ? section.data.resources.map((resource) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
|
|
276
|
+
/* @__PURE__ */ jsx(Typography, { variant: "subtitle1", children: resource.terraform.address }),
|
|
277
|
+
/* @__PURE__ */ jsx(Chip, { size: "small", label: humanize(resource.status) }),
|
|
278
|
+
/* @__PURE__ */ jsx(Definition, { label: "File", value: resource.terraform.fileRef ?? "" }),
|
|
279
|
+
/* @__PURE__ */ jsx(
|
|
280
|
+
Definition,
|
|
281
|
+
{
|
|
282
|
+
label: "State links",
|
|
283
|
+
value: String(resource.states.length)
|
|
284
|
+
}
|
|
285
|
+
),
|
|
286
|
+
/* @__PURE__ */ jsx(
|
|
287
|
+
Definition,
|
|
288
|
+
{
|
|
289
|
+
label: "Cloud links",
|
|
290
|
+
value: String(resource.clouds.length)
|
|
291
|
+
}
|
|
292
|
+
),
|
|
293
|
+
resource.terraform.githubUrl && /* @__PURE__ */ jsx(Link, { to: resource.terraform.githubUrl, children: "Open source" })
|
|
294
|
+
] }, resource.terraform.hashedID)) : /* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: "No Terraform provenance is linked to this resource." }) }) });
|
|
295
|
+
const DriftSection = ({
|
|
296
|
+
section
|
|
297
|
+
}) => /* @__PURE__ */ jsx(InfoCard, { title: "IaC drift evidence", children: /* @__PURE__ */ jsx(SectionState, { section, children: section.data?.items?.length ? section.data.items.map((item) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
|
|
298
|
+
/* @__PURE__ */ jsx(Typography, { variant: "subtitle1", children: item.terraform.address }),
|
|
299
|
+
/* @__PURE__ */ jsx(
|
|
300
|
+
Chip,
|
|
301
|
+
{
|
|
302
|
+
size: "small",
|
|
303
|
+
color: item.status === "drifted" ? "secondary" : "default",
|
|
304
|
+
label: humanize(item.status)
|
|
305
|
+
}
|
|
306
|
+
),
|
|
307
|
+
/* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: item.status === "unknown" ? `Unknown is not drift: ${humanize(item.reason)}.` : `${item.comparedFields} supported fields compared.` }),
|
|
308
|
+
item.differences.length > 0 && /* @__PURE__ */ jsxs("details", { children: [
|
|
309
|
+
/* @__PURE__ */ jsx("summary", { children: "View sanitized differences" }),
|
|
310
|
+
/* @__PURE__ */ jsx("ul", { children: item.differences.map((difference) => /* @__PURE__ */ jsxs("li", { children: [
|
|
311
|
+
/* @__PURE__ */ jsx("code", { children: difference.field }),
|
|
312
|
+
":",
|
|
313
|
+
" ",
|
|
314
|
+
difference.lastApplied ?? "\u2205",
|
|
315
|
+
" \u2192",
|
|
316
|
+
" ",
|
|
317
|
+
difference.observed ?? "\u2205"
|
|
318
|
+
] }, difference.field)) })
|
|
319
|
+
] })
|
|
320
|
+
] }, item.terraform.hashedID)) : /* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: "No comparable Terraform evidence is available." }) }) });
|
|
321
|
+
const SectionState = ({
|
|
322
|
+
section,
|
|
323
|
+
children
|
|
324
|
+
}) => {
|
|
325
|
+
if (section.state === "error") {
|
|
326
|
+
return /* @__PURE__ */ jsx(Alert, { severity: "error", children: section.message });
|
|
327
|
+
}
|
|
328
|
+
if (section.state === "unsupported") {
|
|
329
|
+
return /* @__PURE__ */ jsx(Alert, { severity: "info", children: section.message });
|
|
330
|
+
}
|
|
331
|
+
if (section.state === "empty") {
|
|
332
|
+
return /* @__PURE__ */ jsx(Typography, { color: "textSecondary", children: section.message });
|
|
333
|
+
}
|
|
334
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
335
|
+
};
|
|
336
|
+
const Definition = ({ label, value }) => /* @__PURE__ */ jsxs(Typography, { paragraph: true, children: [
|
|
337
|
+
/* @__PURE__ */ jsxs("strong", { children: [
|
|
338
|
+
label,
|
|
339
|
+
":"
|
|
340
|
+
] }),
|
|
341
|
+
" ",
|
|
342
|
+
value || "Not available"
|
|
343
|
+
] });
|
|
344
|
+
function humanize(value) {
|
|
345
|
+
return value.replaceAll("_", " ");
|
|
346
|
+
}
|
|
347
|
+
function formatCloudTimestamp(value) {
|
|
348
|
+
const directTimestamp = Date.parse(value);
|
|
349
|
+
if (!Number.isNaN(directTimestamp)) {
|
|
350
|
+
return new Date(directTimestamp).toLocaleString();
|
|
351
|
+
}
|
|
352
|
+
const withoutBracketedZone = value.replace(/\[[^\]]+\]$/, "");
|
|
353
|
+
const normalizedTimestamp = Date.parse(withoutBracketedZone);
|
|
354
|
+
return Number.isNaN(normalizedTimestamp) ? value : new Date(normalizedTimestamp).toLocaleString();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export { CloudResourceBriefView, CloudResourceContent, cloudResourceIdentity, formatCloudTimestamp };
|
|
358
|
+
//# sourceMappingURL=CloudResourceContent.esm.js.map
|