@terasky/backstage-plugin-vcf-automation 1.2.0 → 1.3.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.
@@ -1,11 +1,18 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import { useEntity } from '@backstage/plugin-catalog-react';
2
+ import { useState, useMemo, useCallback } from 'react';
3
+ import { useEntity, catalogApiRef } from '@backstage/plugin-catalog-react';
4
+ import { useApi } from '@backstage/core-plugin-api';
5
+ import { usePermission } from '@backstage/plugin-permission-react';
6
+ import { useAsync } from 'react-use';
7
+ import Editor from '@monaco-editor/react';
3
8
  import { InfoCard, StructuredMetadataTable, Table, CodeSnippet, StatusPending, StatusError, StatusOK } from '@backstage/core-components';
4
- import { Typography, Grid, Card, CardContent, Tabs, Tab, Box, Chip, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core';
9
+ import { Typography, Grid, Card, CardContent, Tabs, Tab, Box, Chip, Accordion, AccordionSummary, AccordionDetails, Button, Dialog, DialogTitle, DialogContent, DialogActions, Snackbar } from '@material-ui/core';
10
+ import { Alert } from '@material-ui/lab';
5
11
  import { makeStyles } from '@material-ui/core/styles';
6
12
  import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
7
- import yaml from 'js-yaml';
8
- import { useState } from 'react';
13
+ import * as yaml from 'js-yaml';
14
+ import { vcfAutomationApiRef } from '../api/VcfAutomationClient.esm.js';
15
+ import { supervisorResourceEditPermission } from '@terasky/backstage-plugin-vcf-automation-common';
9
16
 
10
17
  const useStyles = makeStyles((theme) => ({
11
18
  statusChip: {
@@ -35,6 +42,33 @@ const useStyles = makeStyles((theme) => ({
35
42
  },
36
43
  tabPanel: {
37
44
  paddingTop: theme.spacing(2)
45
+ },
46
+ yamlEditorContainer: {
47
+ height: "70vh",
48
+ minHeight: "500px",
49
+ display: "flex",
50
+ flexDirection: "column"
51
+ },
52
+ monacoEditor: {
53
+ flex: 1,
54
+ border: `1px solid ${theme.palette.divider}`,
55
+ borderRadius: theme.shape.borderRadius
56
+ },
57
+ validationStatus: {
58
+ padding: theme.spacing(1),
59
+ borderTop: `1px solid ${theme.palette.divider}`,
60
+ backgroundColor: theme.palette.background.paper,
61
+ flexShrink: 0
62
+ },
63
+ yamlValidationError: {
64
+ color: theme.palette.error.main,
65
+ fontSize: "0.875rem"
66
+ },
67
+ editorActions: {
68
+ display: "flex",
69
+ gap: theme.spacing(1),
70
+ marginTop: theme.spacing(2),
71
+ justifyContent: "flex-end"
38
72
  }
39
73
  }));
40
74
  function TabPanel(props) {
@@ -55,21 +89,273 @@ const VCFAutomationCCIResourceDetails = () => {
55
89
  const classes = useStyles();
56
90
  const { entity } = useEntity();
57
91
  const [tabValue, setTabValue] = useState(0);
58
- const resourceProperties = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-properties"];
59
- const resourceManifest = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-cci-resource-manifest"];
60
- const resourceObject = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-cci-resource-object"];
61
- const resourceContext = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-cci-resource-context"];
62
- const resourceState = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-state"];
63
- const syncStatus = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-sync-status"];
64
- const createdAt = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-created-at"];
65
- const origin = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-origin"];
66
- const resourceData = resourceProperties ? JSON.parse(resourceProperties) : null;
67
- const manifest = resourceManifest ? JSON.parse(resourceManifest) : null;
68
- const objectData = resourceObject ? JSON.parse(resourceObject) : null;
92
+ const api = useApi(vcfAutomationApiRef);
93
+ const catalogApi = useApi(catalogApiRef);
94
+ const { allowed: canEditResource } = usePermission({
95
+ permission: supervisorResourceEditPermission
96
+ });
97
+ const [editingYaml, setEditingYaml] = useState("");
98
+ const [originalManifest, setOriginalManifest] = useState(null);
99
+ const [isLoadingManifest, setIsLoadingManifest] = useState(false);
100
+ const [isSaving, setIsSaving] = useState(false);
101
+ const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
102
+ const [yamlValidationError, setYamlValidationError] = useState("");
103
+ const [snackbar, setSnackbar] = useState({
104
+ open: false,
105
+ message: "",
106
+ severity: "success"
107
+ });
108
+ const deploymentId = entity.spec?.system;
109
+ const resourceId = entity.metadata.name;
110
+ const instanceName = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-instance"];
111
+ const isStandalone = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-origin"] === "STANDALONE";
112
+ const annotationData = useMemo(() => {
113
+ const resourceProperties = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-properties"];
114
+ const resourceManifest = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-cci-resource-manifest"];
115
+ const resourceObject = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-cci-resource-object"];
116
+ const resourceContext2 = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-cci-resource-context"];
117
+ const resourceState2 = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-state"];
118
+ const syncStatus2 = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-sync-status"];
119
+ const createdAt2 = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-created-at"];
120
+ const origin2 = entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-origin"];
121
+ return {
122
+ resourceData: resourceProperties && resourceProperties !== "{}" ? JSON.parse(resourceProperties) : null,
123
+ manifest: resourceManifest && resourceManifest !== "{}" ? JSON.parse(resourceManifest) : null,
124
+ objectData: resourceObject && resourceObject !== "{}" ? JSON.parse(resourceObject) : null,
125
+ resourceContext: resourceContext2 || "",
126
+ resourceState: resourceState2,
127
+ syncStatus: syncStatus2,
128
+ createdAt: createdAt2,
129
+ origin: origin2
130
+ };
131
+ }, [
132
+ entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-properties"],
133
+ entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-cci-resource-manifest"],
134
+ entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-cci-resource-object"],
135
+ entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-cci-resource-context"],
136
+ entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-state"],
137
+ entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-sync-status"],
138
+ entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-created-at"],
139
+ entity.metadata.annotations?.["terasky.backstage.io/vcf-automation-resource-origin"]
140
+ ]);
141
+ const needsApiCall = !annotationData.resourceData || !annotationData.manifest || !annotationData.objectData;
142
+ const { value: apiResourceData, loading, error } = useAsync(async () => {
143
+ if (!needsApiCall || !resourceId) {
144
+ return null;
145
+ }
146
+ try {
147
+ if (isStandalone) {
148
+ const response = await api.getSupervisorResource(resourceId, instanceName);
149
+ if (response) {
150
+ return {
151
+ id: response.id,
152
+ properties: {
153
+ manifest: {
154
+ apiVersion: response.apiVersion,
155
+ kind: response.kind,
156
+ metadata: response.metadata,
157
+ spec: response.spec
158
+ },
159
+ object: {
160
+ apiVersion: response.apiVersion,
161
+ kind: response.kind,
162
+ metadata: response.metadata,
163
+ spec: response.spec,
164
+ status: response.status
165
+ },
166
+ context: JSON.stringify({
167
+ namespace: response.metadata.namespace,
168
+ apiVersion: response.apiVersion,
169
+ kind: response.kind,
170
+ standalone: true
171
+ })
172
+ }
173
+ };
174
+ }
175
+ } else {
176
+ if (!deploymentId) {
177
+ return null;
178
+ }
179
+ const response = await api.getDeploymentResources(deploymentId, instanceName);
180
+ let resources = null;
181
+ if (response) {
182
+ if (Array.isArray(response)) {
183
+ resources = response;
184
+ } else if (response.content && Array.isArray(response.content)) {
185
+ resources = response.content;
186
+ }
187
+ }
188
+ if (resources) {
189
+ return resources.find((r) => r.id === resourceId);
190
+ }
191
+ }
192
+ return null;
193
+ } catch (apiError) {
194
+ console.error("Failed to fetch resource data:", apiError);
195
+ return null;
196
+ }
197
+ }, [needsApiCall, isStandalone, deploymentId, resourceId, instanceName]);
198
+ const resourceData = annotationData.resourceData || apiResourceData;
199
+ const manifest = annotationData.manifest || apiResourceData?.properties?.manifest;
200
+ const objectData = annotationData.objectData || apiResourceData?.properties?.object;
201
+ const resourceContext = annotationData.resourceContext || apiResourceData?.properties?.context;
202
+ const resourceState = annotationData.resourceState;
203
+ const syncStatus = annotationData.syncStatus;
204
+ const createdAt = annotationData.createdAt;
205
+ const origin = annotationData.origin;
206
+ const resourceKind = manifest?.kind || objectData?.kind;
207
+ const resourceName = manifest?.metadata?.name || objectData?.metadata?.name;
208
+ const namespaceName = manifest?.metadata?.namespace || objectData?.metadata?.namespace;
209
+ const apiVersion = manifest?.apiVersion || objectData?.apiVersion;
210
+ const extractNamespaceUrnId = useCallback((endpoint) => {
211
+ const match = endpoint.match(/\/namespaces\/(urn:vcloud:namespace:[^\/]+)/);
212
+ return match ? match[1] : void 0;
213
+ }, []);
214
+ const findCCINamespaceParent = useCallback(async (currentEntity, depth = 0) => {
215
+ if (depth >= 3) return void 0;
216
+ if (currentEntity.spec?.type === "CCI.Supervisor.Namespace") {
217
+ const endpoint = currentEntity.metadata?.annotations?.["terasky.backstage.io/vcf-automation-cci-namespace-endpoint"];
218
+ if (endpoint) {
219
+ return extractNamespaceUrnId(endpoint);
220
+ }
221
+ }
222
+ const parentRef = currentEntity.spec?.subcomponentOf;
223
+ if (!parentRef) return void 0;
224
+ try {
225
+ const parentEntity = await catalogApi.getEntityByRef(parentRef);
226
+ if (parentEntity) {
227
+ return await findCCINamespaceParent(parentEntity, depth + 1);
228
+ }
229
+ } catch (error2) {
230
+ console.warn("Failed to fetch parent entity:", parentRef, error2);
231
+ }
232
+ return void 0;
233
+ }, [catalogApi, extractNamespaceUrnId]);
234
+ const { value: namespaceUrnId } = useAsync(async () => {
235
+ if (!namespaceName) return void 0;
236
+ if (isStandalone) {
237
+ let contextData = null;
238
+ try {
239
+ contextData = typeof resourceContext === "string" ? JSON.parse(resourceContext || "{}") : resourceContext;
240
+ } catch (error2) {
241
+ console.log("Debug - Resource context is not JSON, treating as string:", resourceContext);
242
+ contextData = null;
243
+ }
244
+ return contextData?.namespaceUrnId || namespaceName;
245
+ } else {
246
+ const urnId = await findCCINamespaceParent(entity);
247
+ return urnId || namespaceName;
248
+ }
249
+ }, [namespaceName, resourceContext, isStandalone, entity, findCCINamespaceParent]);
250
+ const loadManifestForEditing = useCallback(async () => {
251
+ if (!canEditResource || !namespaceName || !resourceName || !namespaceUrnId || !apiVersion || !resourceKind) {
252
+ return;
253
+ }
254
+ setIsLoadingManifest(true);
255
+ try {
256
+ const manifestResponse = await api.getSupervisorResourceManifest(
257
+ namespaceUrnId,
258
+ namespaceName,
259
+ resourceName,
260
+ apiVersion,
261
+ resourceKind,
262
+ instanceName
263
+ );
264
+ setOriginalManifest(manifestResponse);
265
+ const yamlContent = yaml.dump(manifestResponse, {
266
+ indent: 2,
267
+ lineWidth: -1,
268
+ noRefs: true,
269
+ sortKeys: false
270
+ });
271
+ setEditingYaml(yamlContent);
272
+ setYamlValidationError("");
273
+ } catch (error2) {
274
+ setSnackbar({
275
+ open: true,
276
+ message: `Failed to fetch resource manifest: ${error2 instanceof Error ? error2.message : "Unknown error"}`,
277
+ severity: "error"
278
+ });
279
+ } finally {
280
+ setIsLoadingManifest(false);
281
+ }
282
+ }, [canEditResource, namespaceName, resourceName, namespaceUrnId, apiVersion, resourceKind, instanceName, api]);
283
+ const validateYaml = useCallback((yamlString) => {
284
+ try {
285
+ yaml.load(yamlString);
286
+ setYamlValidationError("");
287
+ return true;
288
+ } catch (error2) {
289
+ const errorMessage = error2 instanceof Error ? error2.message : "Invalid YAML syntax";
290
+ setYamlValidationError(errorMessage);
291
+ return false;
292
+ }
293
+ }, []);
294
+ const handleYamlChange = useCallback((value) => {
295
+ setEditingYaml(value);
296
+ if (value.trim()) {
297
+ validateYaml(value);
298
+ } else {
299
+ setYamlValidationError("");
300
+ }
301
+ }, [validateYaml]);
302
+ const handleSaveResource = useCallback(async () => {
303
+ if (!originalManifest || !namespaceName || !resourceName || !namespaceUrnId || !apiVersion || !resourceKind) {
304
+ return;
305
+ }
306
+ setIsSaving(true);
307
+ setConfirmDialogOpen(false);
308
+ try {
309
+ const updatedManifest = yaml.load(editingYaml);
310
+ await api.updateSupervisorResourceManifest(
311
+ namespaceUrnId,
312
+ namespaceName,
313
+ resourceName,
314
+ apiVersion,
315
+ resourceKind,
316
+ updatedManifest,
317
+ instanceName
318
+ );
319
+ setSnackbar({
320
+ open: true,
321
+ message: "Resource manifest updated successfully",
322
+ severity: "success"
323
+ });
324
+ setTimeout(() => window.location.reload(), 1e3);
325
+ } catch (error2) {
326
+ setSnackbar({
327
+ open: true,
328
+ message: `Failed to update resource manifest: ${error2 instanceof Error ? error2.message : "Unknown error"}`,
329
+ severity: "error"
330
+ });
331
+ } finally {
332
+ setIsSaving(false);
333
+ }
334
+ }, [originalManifest, namespaceName, resourceName, namespaceUrnId, apiVersion, resourceKind, editingYaml, instanceName, api]);
335
+ const handleCancelEditing = useCallback(() => {
336
+ setEditingYaml("");
337
+ setOriginalManifest(null);
338
+ setYamlValidationError("");
339
+ }, []);
340
+ const handleCloseSnackbar = useCallback(() => {
341
+ setSnackbar((prev) => ({ ...prev, open: false }));
342
+ }, []);
69
343
  const handleTabChange = (_event, newValue) => {
70
344
  setTabValue(newValue);
345
+ if (newValue === 5 && canEditResource && !editingYaml && !isLoadingManifest) {
346
+ loadManifestForEditing();
347
+ }
71
348
  };
72
- if (!resourceData) {
349
+ if (loading) {
350
+ return /* @__PURE__ */ jsx(InfoCard, { title: "CCI Supervisor Resource Details", children: /* @__PURE__ */ jsx(Typography, { children: "Loading resource details..." }) });
351
+ }
352
+ if (error) {
353
+ return /* @__PURE__ */ jsx(InfoCard, { title: "CCI Supervisor Resource Details", children: /* @__PURE__ */ jsxs(Typography, { color: "error", children: [
354
+ "Error loading resource details: ",
355
+ error.message
356
+ ] }) });
357
+ }
358
+ if (!resourceData && !manifest && !objectData) {
73
359
  return /* @__PURE__ */ jsx(InfoCard, { title: "CCI Supervisor Resource Details", children: /* @__PURE__ */ jsx(Typography, { children: "No resource data available." }) });
74
360
  }
75
361
  const renderStatusIcon = (conditionStatus) => {
@@ -109,7 +395,7 @@ const VCFAutomationCCIResourceDetails = () => {
109
395
  noRefs: true,
110
396
  sortKeys: false
111
397
  });
112
- } catch (error) {
398
+ } catch (error2) {
113
399
  return JSON.stringify(data, null, 2);
114
400
  }
115
401
  };
@@ -229,7 +515,8 @@ const VCFAutomationCCIResourceDetails = () => {
229
515
  /* @__PURE__ */ jsx(Tab, { label: "Manifest Details" }),
230
516
  /* @__PURE__ */ jsx(Tab, { label: "Object Status" }),
231
517
  /* @__PURE__ */ jsx(Tab, { label: "Conditions" }),
232
- /* @__PURE__ */ jsx(Tab, { label: "YAML Views" })
518
+ /* @__PURE__ */ jsx(Tab, { label: "YAML Views" }),
519
+ canEditResource && resourceName && namespaceName && namespaceUrnId && apiVersion && /* @__PURE__ */ jsx(Tab, { label: "Edit Manifest" })
233
520
  ] }),
234
521
  /* @__PURE__ */ jsx(TabPanel, { value: tabValue, index: 0, children: /* @__PURE__ */ jsxs(Grid, { container: true, spacing: 3, children: [
235
522
  /* @__PURE__ */ jsx(Grid, { item: true, xs: 12, children: /* @__PURE__ */ jsx(StructuredMetadataTable, { metadata: basicInfo }) }),
@@ -350,7 +637,101 @@ const VCFAutomationCCIResourceDetails = () => {
350
637
  }
351
638
  ) }) })
352
639
  ] }) })
353
- ] }) })
640
+ ] }) }),
641
+ canEditResource && resourceName && namespaceName && namespaceUrnId && apiVersion && /* @__PURE__ */ jsx(TabPanel, { value: tabValue, index: 5, children: /* @__PURE__ */ jsx(Grid, { container: true, spacing: 3, children: /* @__PURE__ */ jsxs(Grid, { item: true, xs: 12, children: [
642
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", className: classes.sectionTitle, children: "Edit Resource Manifest" }),
643
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "textSecondary", gutterBottom: true, children: [
644
+ resourceName,
645
+ " (",
646
+ resourceKind,
647
+ ")"
648
+ ] }),
649
+ isLoadingManifest ? /* @__PURE__ */ jsx(Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "400px", children: /* @__PURE__ */ jsx(Typography, { children: "Loading manifest..." }) }) : /* @__PURE__ */ jsxs(Box, { className: classes.yamlEditorContainer, children: [
650
+ /* @__PURE__ */ jsx(Box, { className: classes.monacoEditor, children: /* @__PURE__ */ jsx(
651
+ Editor,
652
+ {
653
+ height: "100%",
654
+ defaultLanguage: "yaml",
655
+ value: editingYaml,
656
+ onChange: (value) => handleYamlChange(value || ""),
657
+ theme: "vs-dark",
658
+ options: {
659
+ minimap: { enabled: false },
660
+ scrollBeyondLastLine: false,
661
+ fontSize: 14,
662
+ lineNumbers: "on",
663
+ wordWrap: "off",
664
+ automaticLayout: true,
665
+ tabSize: 2,
666
+ insertSpaces: true,
667
+ folding: true,
668
+ renderWhitespace: "selection"
669
+ }
670
+ }
671
+ ) }),
672
+ /* @__PURE__ */ jsx(Box, { className: classes.validationStatus, children: yamlValidationError ? /* @__PURE__ */ jsxs(Typography, { className: classes.yamlValidationError, children: [
673
+ "\u26A0\uFE0F YAML Validation Error: ",
674
+ yamlValidationError
675
+ ] }) : editingYaml.trim() ? /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "textSecondary", children: "\u2705 YAML syntax is valid" }) : /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "textSecondary", children: "Enter YAML content above" }) }),
676
+ /* @__PURE__ */ jsxs(Box, { className: classes.editorActions, children: [
677
+ /* @__PURE__ */ jsx(
678
+ Button,
679
+ {
680
+ variant: "outlined",
681
+ onClick: handleCancelEditing,
682
+ disabled: isSaving,
683
+ children: "Cancel"
684
+ }
685
+ ),
686
+ /* @__PURE__ */ jsx(
687
+ Button,
688
+ {
689
+ variant: "contained",
690
+ color: "primary",
691
+ onClick: () => setConfirmDialogOpen(true),
692
+ disabled: !editingYaml.trim() || !!yamlValidationError || isSaving,
693
+ children: isSaving ? "Saving..." : "Save Changes"
694
+ }
695
+ )
696
+ ] })
697
+ ] })
698
+ ] }) }) }),
699
+ /* @__PURE__ */ jsxs(
700
+ Dialog,
701
+ {
702
+ open: confirmDialogOpen,
703
+ onClose: () => setConfirmDialogOpen(false),
704
+ maxWidth: "sm",
705
+ fullWidth: true,
706
+ children: [
707
+ /* @__PURE__ */ jsx(DialogTitle, { children: "Confirm Changes" }),
708
+ /* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsx(Typography, { children: "Are you sure you want to apply these changes to the resource? This action will update the Kubernetes resource based on your modifications." }) }),
709
+ /* @__PURE__ */ jsxs(DialogActions, { children: [
710
+ /* @__PURE__ */ jsx(Button, { onClick: () => setConfirmDialogOpen(false), color: "primary", children: "Cancel" }),
711
+ /* @__PURE__ */ jsx(
712
+ Button,
713
+ {
714
+ onClick: handleSaveResource,
715
+ color: "primary",
716
+ variant: "contained",
717
+ disabled: isSaving,
718
+ children: isSaving ? "Applying..." : "Apply Changes"
719
+ }
720
+ )
721
+ ] })
722
+ ]
723
+ }
724
+ ),
725
+ /* @__PURE__ */ jsx(
726
+ Snackbar,
727
+ {
728
+ open: snackbar.open,
729
+ autoHideDuration: 6e3,
730
+ onClose: handleCloseSnackbar,
731
+ anchorOrigin: { vertical: "bottom", horizontal: "left" },
732
+ children: /* @__PURE__ */ jsx(Alert, { onClose: handleCloseSnackbar, severity: snackbar.severity, children: snackbar.message })
733
+ }
734
+ )
354
735
  ] }) })
355
736
  ] });
356
737
  };