diff --git a/src/App.tsx b/src/App.tsx index 2137afa..c36dd5a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,8 +25,14 @@ import RoleWizardManager from './components/RoleWizardManager'; import { ProjectSettingsManager } from './components/ProjectSettingsManager'; import { YouTubePopup } from './components/YouTubePopup'; import { DockerRunPopup } from './components/DockerRunPopup'; -import { generateMultiDeploymentYaml } from './utils/yamlGenerator'; +import { generateMultiDeploymentYaml, generateCustomResourceYaml } from './utils/yamlGenerator'; import { JobManager, Job } from './components/JobManager'; +import { CrdManager } from './components/CrdManager'; +import { CrdList } from './components/CrdList'; +import { CrdYamlViewer } from './components/CrdYamlViewer'; +import { CustomResourceForm } from './components/CustomResourceForm'; +import { CustomResourcesList } from './components/CustomResourcesList'; +import { CustomResourceDefinition, CustomResource } from './types'; import { JobList } from './components/jobs/JobList'; import { CronJobList } from './components/jobs/CronJobList'; import type { DeploymentConfig, DaemonSetConfig, Namespace, ConfigMap, Secret, ServiceAccount, ProjectSettings, JobConfig, CronJobConfig, KubernetesRole, KubernetesClusterRole, RoleBinding } from './types'; @@ -48,7 +54,18 @@ import { RoleBindingManager } from './components/RoleBindingManager'; const isPlayground = typeof window !== 'undefined' && window.location.search.includes('q=playground'); type PreviewMode = 'visual' | 'yaml' | 'summary' | 'argocd' | 'flow'; -type SidebarTab = 'deployments' | 'daemonsets' | 'namespaces' | 'storage' | 'security' | 'jobs' | 'configmaps' | 'secrets' | 'roles'; +type SidebarTab = + | 'deployments' + | 'daemonsets' + | 'namespaces' + | 'storage' + | 'security' + | 'jobs' + | 'configmaps' + | 'secrets' + | 'roles' + | 'advanced' + | 'crds'; function App() { const hideDemoIcons = import.meta.env.VITE_HIDE_DEMO_ICONS === 'true'; @@ -112,7 +129,7 @@ function App() { const [showUploadModal, setShowUploadModal] = useState(false); const [showClearModal, setShowClearModal] = useState(false); // Only one group open at a time: 'workloads' | 'storage' | 'security' | null (all collapsed) - const [openGroup, setOpenGroup] = useState<'workloads' | 'storage' | 'security' | null>(null); + const [openGroup, setOpenGroup] = useState<'workloads' | 'storage' | 'security' | 'advanced' | null>(null); const [jobToEdit, setJobToEdit] = useState(undefined); const [selectedJob, setSelectedJob] = useState(-1); const [selectedCronJob, setSelectedCronJob] = useState(-1); @@ -124,6 +141,53 @@ function App() { const [reopenRoleBindingAfterRole, setReopenRoleBindingAfterRole] = useState(false); const [selectedRoleBindingIndex, setSelectedRoleBindingIndex] = useState(-1); const [deleteRoleBindingConfirm, setDeleteRoleBindingConfirm] = useState(null); + + // CRD Management + const [crds, setCrds] = useState([]); + const [selectedCrd, setSelectedCrd] = useState(0); + const [showCrdManager, setShowCrdManager] = useState(false); + const [crdToView, setCrdToView] = useState(null); + + // Custom Resources Management + const [customResources, setCustomResources] = useState([]); + const [showCustomResourceForm, setShowCustomResourceForm] = useState(null); + const [selectedCustomResource, setSelectedCustomResource] = useState(-1); + const [editingCustomResource, setEditingCustomResource] = useState(null); + const [customResourceToView, setCustomResourceToView] = useState(null); + + // Custom Resource handlers + const handleSaveCustomResource = (cr: CustomResource) => { + setCustomResources(prev => { + const existingIndex = prev.findIndex(existing => existing.id === cr.id); + if (existingIndex >= 0) { + const updated = [...prev]; + updated[existingIndex] = cr; + return updated; + } else { + return [...prev, cr]; + } + }); + forceSave(); + }; + + const handleDeleteCustomResource = (index: number) => { + const updated = [...customResources]; + updated.splice(index, 1); + setCustomResources(updated); + forceSave(); + }; + + const handleEditCustomResource = (cr: CustomResource) => { + const crd = crds.find(c => c.id === cr.crdId); + if (crd) { + setEditingCustomResource(cr); + setShowCustomResourceForm(crd); + } + }; + + const handleViewCustomResourceYaml = (cr: CustomResource) => { + setCustomResourceToView(cr); + }; // Auto-save functionality const autoSaveTimeoutRef = useRef(null); @@ -143,6 +207,8 @@ function App() { clusterRoles, namespaces, projectSettings, + crds, + customResources, generatedYaml }; const success = saveConfig(config); @@ -155,7 +221,7 @@ function App() { console.warn('Force save failed:', e); return false; } - }, [deployments, daemonSets, jobs, configMaps, secrets, serviceAccounts, roles, clusterRoles, namespaces, projectSettings, generatedYaml]); + }, [deployments, daemonSets, jobs, configMaps, secrets, serviceAccounts, roles, clusterRoles, namespaces, projectSettings, crds, generatedYaml]); // Auto-save function const autoSave = useCallback(() => { @@ -176,8 +242,10 @@ function App() { clusterRoles, namespaces, projectSettings, - generatedYaml, - roleBindings + generatedYaml, + roleBindings, + crds, + customResources }; const success = saveConfig(config); if (success) { @@ -187,7 +255,7 @@ function App() { console.warn('Auto-save failed:', e); } }, 3000); // 3 second delay - }, [deployments, daemonSets, jobs, configMaps, secrets, serviceAccounts, roles, clusterRoles, namespaces, projectSettings, generatedYaml, roleBindings]); + }, [deployments, daemonSets, jobs, configMaps, secrets, serviceAccounts, roles, clusterRoles, namespaces, projectSettings, generatedYaml, roleBindings, crds]); // Update generated YAML when configuration changes useEffect(() => { @@ -209,7 +277,7 @@ function App() { const yaml = getPreviewYaml(); setGeneratedYaml(yaml); } - }, [deployments, daemonSets, jobs, configMaps, secrets, serviceAccounts, roles, clusterRoles, namespaces, projectSettings, roleBindings]); + }, [deployments, daemonSets, jobs, configMaps, secrets, serviceAccounts, roles, clusterRoles, namespaces, projectSettings, roleBindings, crds]); // Trigger auto-save when any configuration changes useEffect(() => { @@ -233,6 +301,7 @@ function App() { if (saved.jobs) setJobs(saved.jobs); if (saved.generatedYaml) setGeneratedYaml(saved.generatedYaml); if (saved.roleBindings) setRoleBindings(saved.roleBindings); + if (saved.crds) setCrds(saved.crds); console.log('Configuration loaded from localStorage'); } else if (typeof window !== 'undefined' && window.location.search.includes('q=playground')) { setGeneratedYaml(`# Playground Mode\n# Example Deployment\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: playground-deployment\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: playground\n template:\n metadata:\n labels:\n app: playground\n spec:\n containers:\n - name: playground\n image: nginx:latest\n`); @@ -926,7 +995,9 @@ function App() { [], roles, clusterRoles, - roleBindings // Pass roleBindings here + roleBindings, // Pass roleBindings here + crds, // Pass CRDs here + customResources // Pass Custom Resources here ); let finalYaml = yaml; @@ -941,7 +1012,9 @@ function App() { serviceAccounts.length === 0 && roles.length === 0 && clusterRoles.length === 0 && - roleBindings.length === 0 // Add this check + roleBindings.length === 0 && + crds.length === 0 && + customResources.length === 0 // Add CRDs check ) { finalYaml = '# No resources configured\n# Create your first deployment, daemonset, job, service account, configmap, or secret to see the generated YAML'; } @@ -1003,7 +1076,7 @@ function App() { } // Function to determine filter type based on current sidebar tab and sub-tabs - const getFilterType = (): 'all' | 'deployments' | 'daemonsets' | 'namespaces' | 'configmaps' | 'secrets' | 'serviceaccounts' | 'roles' | 'rolebindings' | 'jobs' | 'cronjobs' => { + const getFilterType = (): 'all' | 'deployments' | 'daemonsets' | 'namespaces' | 'configmaps' | 'secrets' | 'serviceaccounts' | 'roles' | 'rolebindings' | 'jobs' | 'cronjobs' | 'crds' => { // Show all resources when showAllResources is true if (showAllResources) return 'all'; @@ -1011,6 +1084,7 @@ function App() { if (sidebarTab === 'deployments') return 'deployments'; if (sidebarTab === 'daemonsets') return 'daemonsets'; if (sidebarTab === 'namespaces') return 'namespaces'; + if (sidebarTab === 'crds') return 'crds'; if (sidebarTab === 'jobs') { if (jobsSubTab === 'jobs') return 'jobs'; if (jobsSubTab === 'cronjobs') return 'cronjobs'; @@ -1047,7 +1121,6 @@ function App() { setSecuritySubTab('roles'); } else if (subTab === 'rolebindings') { setSecuritySubTab('rolebindings'); - } else if (subTab === 'jobs') { setJobsSubTab('jobs'); } else if (subTab === 'cronjobs') { @@ -1208,6 +1281,43 @@ function App() { ) : ( )} + {/* Advanced Group */} + + {openGroup === 'advanced' && ( +
+ +
+ )}
@@ -1633,6 +1743,43 @@ function App() {
)} + + {/* Advanced Group */} + + {openGroup === 'advanced' && ( +
+ +
+ )} {/* Tab Content */} @@ -1735,6 +1882,61 @@ function App() { /> )} + + {sidebarTab === 'crds' && ( +
+
+ +
+
+ { + setSelectedCrd(index); + setSidebarOpen(false); + }} + onDelete={(index) => { + const updatedCrds = [...crds]; + updatedCrds.splice(index, 1); + setCrds(updatedCrds); + forceSave(); + }} + onViewYaml={(crd) => setCrdToView(crd)} + onCreateResource={(crd) => setShowCustomResourceForm(crd)} + /> +
+ + {/* Custom Resources Section */} +
+
+

+ Custom Resources +

+
+
+ { + setSelectedCustomResource(index); + setSidebarOpen(false); + }} + onDelete={handleDeleteCustomResource} + onEdit={handleEditCustomResource} + onViewYaml={handleViewCustomResourceYaml} + /> +
+
+
+ )} {sidebarTab === 'storage' && ( <> @@ -1858,6 +2060,8 @@ function App() { )} + + {sidebarTab === 'security' && ( <> {securitySubTab === 'serviceaccounts' && ( @@ -2147,8 +2351,8 @@ function App() {
- {previewMode === 'flow' && } - {previewMode === 'summary' && } + {previewMode === 'flow' && } + {previewMode === 'summary' && } {previewMode === 'yaml' && }
@@ -2277,6 +2481,27 @@ function App() { {/* Service Account Manager Modal */} + + {/* CRD Manager Modal */} + {showCrdManager && ( + { + setCrds([...crds, crd]); + forceSave(); + }} + onClose={() => { + setShowCrdManager(false); + }} + /> + )} + + {/* CRD YAML Viewer Modal */} + {crdToView && ( + setCrdToView(null)} + /> + )} {showServiceAccountManager && ( )} + + {/* Custom Resource Form Modal */} + {showCustomResourceForm && ( + { + setShowCustomResourceForm(null); + setEditingCustomResource(null); + }} + /> + )} + + {/* Custom Resource YAML Viewer Modal */} + {customResourceToView && ( +
+
+
+
+

+ Custom Resource YAML +

+

+ {customResourceToView.kind} - {customResourceToView.name} +

+
+ +
+
+ +
+
+
+ )} ); } diff --git a/src/components/CrdList.tsx b/src/components/CrdList.tsx new file mode 100644 index 0000000..233c017 --- /dev/null +++ b/src/components/CrdList.tsx @@ -0,0 +1,205 @@ +import React, { useState } from 'react'; +import { FileText, Trash2, Code, Copy, Calendar, Eye } from 'lucide-react'; +import { CustomResourceDefinition } from '../types'; +import { CrdSchemaViewer } from './CrdSchemaViewer'; + +// Simple Tooltip Component +interface TooltipProps { + children: React.ReactNode; + content: string; + position?: 'top' | 'bottom' | 'left' | 'right'; +} + +const Tooltip: React.FC = ({ children, content, position = 'top' }) => { + const [isVisible, setIsVisible] = useState(false); + + const positionClasses = { + top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2', + bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2', + left: 'right-full top-1/2 transform -translate-y-1/2 mr-2', + right: 'left-full top-1/2 transform -translate-y-1/2 ml-2' + }; + + return ( +
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + > + {children} + {isVisible && ( +
+
+ {content} +
+
+
+ )} +
+ ); +}; + +interface CrdListProps { + crds: CustomResourceDefinition[]; + selectedIndex: number; + onSelect: (index: number) => void; + onDelete: (index: number) => void; + onViewYaml: (crd: CustomResourceDefinition) => void; + onCreateResource?: (crd: CustomResourceDefinition) => void; +} + +/** + * Component for displaying a list of imported CRDs + */ +export const CrdList: React.FC = ({ + crds, + selectedIndex, + onSelect, + onDelete, + onViewYaml, + onCreateResource +}) => { + const [viewSchemaFor, setViewSchemaFor] = useState(null); + if (crds.length === 0) { + return ( +
+
+ +
+

No Custom Resource Definitions

+

+ Import your first CRD to get started with custom resources +

+
+ ); + } + + return ( +
+ {crds.map((crd, index) => { + // Find the storage version + const storageVersion = crd.versions.find(v => v.storage) || crd.versions[0]; + + return ( +
onSelect(index)} + > + {/* Header Row */} +
+
+ +
+

+ {crd.name.split('.')[0]} +

+
+
+ + {/* Action Buttons */} +
+ + + + + + + + + + + + + + + +
+
+ + {/* Details Row */} +
+
+ {/* Group */} +
+ {crd.group} +
+ + {/* Separator */} + + + {/* Version */} + + v{storageVersion.name} + + + {/* Scope */} + + {crd.scope} + +
+ + {/* Date */} +
+ + {new Date(crd.createdAt).toLocaleDateString()} +
+
+
+ ); + })} + + {/* Schema Viewer Modal */} + {viewSchemaFor && ( + setViewSchemaFor(null)} + /> + )} + +
+ ); +}; diff --git a/src/components/CrdManager.tsx b/src/components/CrdManager.tsx new file mode 100644 index 0000000..348cadc --- /dev/null +++ b/src/components/CrdManager.tsx @@ -0,0 +1,339 @@ +import React, { useState, useRef } from 'react'; +import { X, Upload, AlertCircle, CheckCircle } from 'lucide-react'; +import { CustomResourceDefinition, CRDSummary } from '../types'; +import { parseYaml } from '../utils/yamlParser'; + + // Removed since we're using a simple textarea now + +// Simple YAML syntax highlighter without line numbers (for display purposes) +const highlightYamlSimple = (code: string): React.ReactNode => { + if (!code) return null; + + const lines = code.split('\n'); + + return ( +
+ {lines.map((line, index) => { + return ( +
+ + {line || ' '} + +
+ ); + })} +
+ ); +}; + +interface CrdManagerProps { + onAddCrd: (crd: CustomResourceDefinition) => void; + onClose: () => void; +} + +/** + * Component for importing and managing CRDs + */ +export const CrdManager: React.FC = ({ onAddCrd, onClose }) => { + const [yamlContent, setYamlContent] = useState(''); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const fileInputRef = useRef(null); + const textareaRef = useRef(null); + + // No longer needed since we removed the overlay system + + /** + * Validates and processes imported CRD YAML + * @returns true if valid, false otherwise + */ + const processCrd = (): boolean => { + if (!yamlContent.trim()) { + setError('Please enter or upload a CRD YAML'); + return false; + } + + try { + // Parse YAML to object + const crdObj = parseYaml(yamlContent); + + // Validate basic CRD structure + if (!crdObj.apiVersion || !crdObj.kind) { + setError('Invalid YAML: Missing apiVersion or kind'); + return false; + } + + if (crdObj.kind !== 'CustomResourceDefinition') { + setError('The provided YAML is not a CustomResourceDefinition'); + return false; + } + + if (!crdObj.metadata?.name || !crdObj.spec?.group) { + setError('Invalid CRD: Missing required fields (metadata.name or spec.group)'); + return false; + } + + // Check for versions + if (!crdObj.spec.versions || !Array.isArray(crdObj.spec.versions) || crdObj.spec.versions.length === 0) { + setError('Invalid CRD: Missing or empty spec.versions array'); + return false; + } + + // Get the storage version (the one with storage: true) + const storageVersion = crdObj.spec.versions.find((v: any) => v.storage === true); + if (!storageVersion) { + setError('Invalid CRD: No storage version found in spec.versions'); + return false; + } + + // Note: Schema is available in storageVersion.schema?.openAPIV3Schema for v1 + // or in crdObj.spec.validation?.openAPIV3Schema for v1beta1 + // We extract it when processing the versions array + + // Create the CRD object + const crd: CustomResourceDefinition = { + id: `${Date.now()}`, + apiVersion: crdObj.apiVersion, + kind: crdObj.kind, + name: crdObj.metadata.name, + group: crdObj.spec.group, + scope: crdObj.spec.scope || 'Namespaced', + versions: crdObj.spec.versions.map((v: any) => ({ + name: v.name, + served: v.served, + storage: v.storage, + schema: v.schema?.openAPIV3Schema || null + })), + createdAt: new Date().toISOString(), + rawYaml: yamlContent + }; + + // Create a summary for success message + const summary: CRDSummary = { + id: crd.id, + name: crd.name, + group: crd.group, + kind: crdObj.spec.names.kind, + scope: crd.scope, + version: storageVersion.name, + createdAt: crd.createdAt + }; + + // Add the CRD + onAddCrd(crd); + + // Show success message + setSuccess(summary); + setError(null); + return true; + } catch (err) { + console.error('Error parsing CRD:', err); + setError(`Error parsing CRD: ${err instanceof Error ? err.message : 'Invalid YAML'}`); + return false; + } + }; + + /** + * Handles file uploads + */ + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const content = event.target?.result as string; + setYamlContent(content); + setError(null); + setSuccess(null); + }; + reader.onerror = () => { + setError('Error reading the file'); + }; + reader.readAsText(file); + }; + + /** + * Handles form submission + */ + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (processCrd()) { + // Clear the form after successful submission + setTimeout(() => { + setYamlContent(''); + setSuccess(null); + }, 2000); + } + }; + + return ( +
+
+ {/* Header */} +
+

Import Custom Resource Definition

+ +
+ + {/* Body */} +
+ {error && ( +
+ +

{error}

+
+ )} + + {success && ( +
+ +
+

CRD imported successfully!

+

+ Name: {success.name} +

+

+ Kind: {success.kind} +

+

+ Group: {success.group} +

+

+ Version: {success.version} +

+
+
+ )} + +
+
+ +