From a333535a877d1a62c88f7b870ef8ed0aaf35f73e Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 7 Jul 2026 15:43:20 +0200 Subject: [PATCH 1/8] Align proposal types with API and fix minor lint issues Signed-off-by: Gabriel Bernal --- src/__mocks__/dynamic-plugin-sdk.ts | 8 +++++ src/models/proposal.ts | 52 ++++++++++++++++++++--------- src/test-helpers.ts | 6 +++- start-console.sh | 2 +- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/__mocks__/dynamic-plugin-sdk.ts b/src/__mocks__/dynamic-plugin-sdk.ts index 570c9bd..ecdd586 100644 --- a/src/__mocks__/dynamic-plugin-sdk.ts +++ b/src/__mocks__/dynamic-plugin-sdk.ts @@ -1,5 +1,13 @@ import { vi } from 'vitest'; +export class HttpError extends Error { + code?: number; + constructor(message: string, code?: number) { + super(message); + this.code = code; + } +} + export const k8sPatch = vi.fn(); export const k8sCreate = vi.fn(); export const consoleFetch = vi.fn(); diff --git a/src/models/proposal.ts b/src/models/proposal.ts index 7a3f2b7..6335362 100644 --- a/src/models/proposal.ts +++ b/src/models/proposal.ts @@ -270,7 +270,7 @@ export type SandboxInfo = { export type ProposalCondition = { type: string; - status: string; + status: 'True' | 'False' | 'Unknown'; lastTransitionTime?: string; reason?: string; message?: string; @@ -299,9 +299,25 @@ export type SecretRequirement = { mountAs: SecretMountSpec; }; +export type MCPHeaderConfig = { + name: string; + valueFrom?: { + type: 'Secret' | 'ServiceAccountToken'; + secret?: { name: string }; + }; +}; + +export type MCPServerConfig = { + name: string; + url: string; + timeoutSeconds?: number; + headers?: MCPHeaderConfig[]; +}; + export type ToolsSpec = { skills?: SkillsSource[]; requiredSecrets?: SecretRequirement[]; + mcpServers?: MCPServerConfig[]; }; export type ProposalStep = { @@ -313,7 +329,7 @@ export type ProposalStep = { export type AgentDiagnosis = { summary: string; - confidence: string; + confidence: 'Low' | 'Medium' | 'High'; rootCause: string; }; @@ -325,26 +341,27 @@ export type AgentAction = { export type AgentProposal = { description: string; actions: AgentAction[]; - risk: string; - reversible: boolean; - rollbackPlan?: AgentRollbackPlan | string; + risk: 'Low' | 'Medium' | 'High' | 'Critical'; + reversible?: 'Reversible' | 'Irreversible' | 'Partial'; + estimatedImpact: string; + rollbackPlan?: AgentRollbackPlan; }; export type VerificationStep = { name: string; - command: string; - expected: string; + command?: string; + expected?: string; type: string; }; export type AgentRollbackPlan = { description: string; - command: string; + command?: string; }; export type AgentVerification = { description: string; - steps: VerificationStep[]; + steps?: VerificationStep[]; }; export type PermissionRule = { @@ -393,7 +410,7 @@ export type ExecutionActionTaken = { type: string; description: string; resource?: { apiVersion: string; kind: string; name: string; namespace?: string }; - outcome?: 'Succeeded' | 'Failed'; + outcome: 'Succeeded' | 'Failed'; output?: string; error?: string; }; @@ -476,7 +493,7 @@ export type LightspeedProposal = { // Result CR types — separate CRDs that hold step output data export type ResultCondition = { - type: 'Started' | 'Completed'; + type: string; status: 'True' | 'False' | 'Unknown'; lastTransitionTime: string; reason?: string; @@ -645,12 +662,12 @@ export const derivePhaseFromConditions = (conditions?: ProposalCondition[]): Pro export const getRiskColor = (risk?: string): 'green' | 'orange' | 'red' | 'grey' => { switch (risk) { - case 'low': + case 'Low': return 'green'; - case 'medium': + case 'Medium': return 'orange'; - case 'high': - case 'critical': + case 'High': + case 'Critical': return 'red'; default: return 'grey'; @@ -776,3 +793,8 @@ export type AgentResource = { export type LLMProviderK8s = LLMProviderResource & K8sResourceCommon; export type ApprovalPolicyK8s = ApprovalPolicyResource & K8sResourceCommon; export type AgentK8s = AgentResource & K8sResourceCommon; +export type ProposalK8s = LightspeedProposal & K8sResourceCommon; +export type ProposalApprovalK8s = LightspeedProposalApproval & K8sResourceCommon; +export type AnalysisResultK8s = AnalysisResultCR & K8sResourceCommon; +export type ExecutionResultK8s = ExecutionResultCR & K8sResourceCommon; +export type VerificationResultK8s = VerificationResultCR & K8sResourceCommon; diff --git a/src/test-helpers.ts b/src/test-helpers.ts index 980dc8e..86aa53e 100644 --- a/src/test-helpers.ts +++ b/src/test-helpers.ts @@ -1,6 +1,10 @@ import { ApprovalStage, LightspeedProposalApproval, ProposalCondition } from './models/proposal'; -export function cond(type: string, status: string, reason?: string): ProposalCondition { +export function cond( + type: string, + status: 'True' | 'False' | 'Unknown', + reason?: string, +): ProposalCondition { return { type, status, reason }; } diff --git a/start-console.sh b/start-console.sh index 47c4368..68b1f9f 100755 --- a/start-console.sh +++ b/start-console.sh @@ -7,7 +7,7 @@ CONSOLE_PORT=${CONSOLE_PORT:=9000} CONSOLE_IMAGE_PLATFORM=${CONSOLE_IMAGE_PLATFORM:="linux/amd64"} # Plugin metadata is declared in package.json -PLUGIN_NAME=${npm_package_consolePlugin_name} +PLUGIN_NAME="lightspeed-agentic-console-plugin" echo "Starting local OpenShift console..." From b8d7781a609decdb8befb0eebdfc8ceac2897f38 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 7 Jul 2026 15:43:51 +0200 Subject: [PATCH 2/8] Refactor ProposalDetailPage into section-based layout with hooks Signed-off-by: Gabriel Bernal --- ...in__lightspeed-agentic-console-plugin.json | 163 +- package-lock.json | 456 +++- package.json | 2 + src/components/ConfirmationModal.tsx | 60 + src/components/StatusGuard.tsx | 57 + src/components/proposals/DynamicComponent.tsx | 2 - src/components/proposals/EscalateModal.tsx | 75 - src/components/proposals/MarkdownText.tsx | 7 - src/components/proposals/PhaseIcon.tsx | 60 - .../proposals/ProposalDetailPage.tsx | 2412 +++-------------- src/components/proposals/ProposalListPage.tsx | 15 +- .../proposals/detail/AnalysisSummary.tsx | 134 + .../proposals/detail/ExecutionSummary.tsx | 166 ++ .../proposals/detail/ProposalPhaseLabel.tsx | 9 + .../proposals/detail/ProposalTimeline.tsx | 30 + .../detail/RemediationOptionCard.tsx | 247 ++ .../proposals/detail/SandboxLogViewer.tsx | 127 + .../proposals/detail/StageInProgress.tsx | 28 + .../proposals/detail/VerificationSummary.tsx | 126 + src/components/proposals/detail/detail.css | 3 + src/components/proposals/proposal-detail.css | 183 -- .../proposals/sandbox-log-viewer.css | 28 - src/constants.ts | 3 + src/hooks/useExecutionLogActions.ts | 84 + src/hooks/useProposal.test.ts | 353 +++ src/hooks/useProposal.ts | 479 ++++ src/hooks/useSandboxLogStream.ts | 216 ++ src/models/proposal-views.ts | 105 + src/utils/approval.ts | 11 +- src/utils/proposal-utils.ts | 39 + 30 files changed, 3096 insertions(+), 2584 deletions(-) create mode 100644 src/components/ConfirmationModal.tsx create mode 100644 src/components/StatusGuard.tsx delete mode 100644 src/components/proposals/DynamicComponent.tsx delete mode 100644 src/components/proposals/EscalateModal.tsx delete mode 100644 src/components/proposals/MarkdownText.tsx delete mode 100644 src/components/proposals/PhaseIcon.tsx create mode 100644 src/components/proposals/detail/AnalysisSummary.tsx create mode 100644 src/components/proposals/detail/ExecutionSummary.tsx create mode 100644 src/components/proposals/detail/ProposalPhaseLabel.tsx create mode 100644 src/components/proposals/detail/ProposalTimeline.tsx create mode 100644 src/components/proposals/detail/RemediationOptionCard.tsx create mode 100644 src/components/proposals/detail/SandboxLogViewer.tsx create mode 100644 src/components/proposals/detail/StageInProgress.tsx create mode 100644 src/components/proposals/detail/VerificationSummary.tsx create mode 100644 src/components/proposals/detail/detail.css delete mode 100644 src/components/proposals/proposal-detail.css delete mode 100644 src/components/proposals/sandbox-log-viewer.css create mode 100644 src/constants.ts create mode 100644 src/hooks/useExecutionLogActions.ts create mode 100644 src/hooks/useProposal.test.ts create mode 100644 src/hooks/useProposal.ts create mode 100644 src/hooks/useSandboxLogStream.ts create mode 100644 src/models/proposal-views.ts create mode 100644 src/utils/proposal-utils.ts diff --git a/locales/en/plugin__lightspeed-agentic-console-plugin.json b/locales/en/plugin__lightspeed-agentic-console-plugin.json index afb60dc..32de45a 100644 --- a/locales/en/plugin__lightspeed-agentic-console-plugin.json +++ b/locales/en/plugin__lightspeed-agentic-console-plugin.json @@ -1,11 +1,12 @@ { + "{{count}} remediation options_one": "{{count}} remediation options_one", + "{{count}} remediation options_other": "{{count}} remediation options_other", + "{{label}} not found": "{{label}} not found", + "{{name}} details": "{{name}} details", "{{risk}} risk": "{{risk}} risk", - "Action failed": "Action failed", - "Actions": "Actions", - "Actions Taken": "Actions Taken", + "Actions taken": "Actions taken", "Affected Resources": "Affected Resources", "After": "After", - "After execution, a separate verification agent with read-only cluster access independently checks whether the fix worked. It inspects actual cluster state, not the execution agent's self-reported results. The checks shown below are the analysis agent's recommendations. The verification agent uses its own judgment based on what it observes, and every check it runs is reported transparently in the Verification tab. If verification fails, and retries were selected at approval, the Lightspeed operator will automatically retry execution with the failure reasons included as context for the execution agent.": "After execution, a separate verification agent with read-only cluster access independently checks whether the fix worked. It inspects actual cluster state, not the execution agent's self-reported results. The checks shown below are the analysis agent's recommendations. The verification agent uses its own judgment based on what it observes, and every check it runs is reported transparently in the Verification tab. If verification fails, and retries were selected at approval, the Lightspeed operator will automatically retry execution with the failure reasons included as context for the execution agent.", "Age": "Age", "Agent tiers define the model and settings used at each stage of a proposal workflow.": "Agent tiers define the model and settings used at each stage of a proposal workflow.", "Agentic system is suspended": "Agentic system is suspended", @@ -14,185 +15,119 @@ "Alert Diagnosis: {{name}}": "Alert Diagnosis: {{name}}", "Alert rule created successfully": "Alert rule created successfully", "All operations are halted and new proposals will be terminated. Remove or update the AgenticOLSConfig to resume.": "All operations are halted and new proposals will be terminated. Remove or update the AgenticOLSConfig to resume.", + "Analysis": "Analysis", "Analysis (seconds)": "Analysis (seconds)", - "Analysis did not complete — no proposal was generated.": "Analysis did not complete — no proposal was generated.", - "Analysis Sandbox": "Analysis Sandbox", "Analyzing...": "Analyzing...", - "API Groups": "API Groups", "API Version": "API Version", "Approval Policy": "Approval Policy", "Approval policy saved successfully.": "Approval policy saved successfully.", - "Approval Required": "Approval Required", - "Approve": "Approve", - "Approve Analysis": "Approve Analysis", - "Approve Escalation": "Approve Escalation", - "Approve the escalation step for proposal {{name}}. The agent will research the issue, draft a support case, and file it.": "Approve the escalation step for proposal {{name}}. The agent will research the issue, draft a support case, and file it.", - "Approve Verification": "Approve Verification", - "Approve with {{num}} retries": "Approve with {{num}} retries", - "Auto-scroll": "Auto-scroll", + "Are you sure you want to approve and execute the selected remediation? The agent will apply changes to your cluster.": "Are you sure you want to approve and execute the selected remediation? The agent will apply changes to your cluster.", + "Audit events only": "Audit events only", "Automatic": "Automatic", "Before": "Before", "Cancel": "Cancel", "Chat (seconds)": "Chat (seconds)", - "Close": "Close", - "Cluster Scoped": "Cluster Scoped", - "Completed": "Completed", - "Condition Degraded": "Condition Degraded", - "Condition Improved": "Condition Improved", - "Condition Unchanged": "Condition Unchanged", "Confidence": "Confidence", "Configuration": "Configuration", "Configure whether each workflow stage requires manual approval or runs automatically.": "Configure whether each workflow stage requires manual approval or runs automatically.", - "Confirm Approve": "Confirm Approve", - "Confirm Approve ({{num}} retries)": "Confirm Approve ({{num}} retries)", - "Connection error": "Connection error", + "CONTEXTUAL EVIDENCE": "CONTEXTUAL EVIDENCE", "Create": "Create", "Create Agent": "Create Agent", "Create alert rule": "Create alert rule", "Create LLM Provider": "Create LLM Provider", "Created": "Created", "Credentials Secret Name": "Credentials Secret Name", - "default": "default", "Delete": "Delete", "Deny": "Deny", - "Describe what you want changed about this analysis...": "Describe what you want changed about this analysis...", - "Details": "Details", - "Diagnosis": "Diagnosis", - "Discovering metrics and testing queries...": "Discovering metrics and testing queries...", + "DETECTED ROOT CAUSE": "DETECTED ROOT CAUSE", + "Download plan": "Download plan", "Endpoint": "Endpoint", "Error": "Error", - "Error loading proposal": "Error loading proposal", "Error saving approval policy": "Error saving approval policy", - "Escalate": "Escalate", - "Escalate Proposal": "Escalate Proposal", - "Escalating...": "Escalating...", - "Escalation": "Escalation", - "Escalation requires approval before it can proceed. The agent will research the issue and draft a support case.": "Escalation requires approval before it can proceed. The agent will research the issue and draft a support case.", - "Escalation Result": "Escalation Result", - "Estimated plan": "Estimated plan", + "Estimated impact": "Estimated impact", "Evidence": "Evidence", - "Executing...": "Executing...", + "Execute remediation": "Execute remediation", "Execution": "Execution", "Execution (seconds)": "Execution (seconds)", - "Execution Failed": "Execution Failed", - "Execution is skipped for this proposal": "Execution is skipped for this proposal", - "Execution Sandbox": "Execution Sandbox", - "Execution Succeeded": "Execution Succeeded", "Expected": "Expected", "Expected:": "Expected:", - "Fail": "Fail", "Failed": "Failed", - "Failed to approve escalation": "Failed to approve escalation", "Failed to create alert rule": "Failed to create alert rule", + "Failed to load logs.": "Failed to load logs.", "Failure reason": "Failure reason", - "Full escalation content": "Full escalation content", - "Hide analysis logs": "Hide analysis logs", - "Hide escalation logs": "Hide escalation logs", - "Hide execution logs": "Hide execution logs", + "Hide {{title}} logs": "Hide {{title}} logs", "Hide Timeouts": "Hide Timeouts", - "Hide verification logs": "Hide verification logs", "Impact": "Impact", - "Independent verification": "Independent verification", - "Justification": "Justification", + "In progress": "In progress", "Large language model providers available to agents for proposal analysis and execution.": "Large language model providers available to agents for proposal analysis and execution.", - "Live": "Live", "LLM Provider": "LLM Provider", "LLM Providers": "LLM Providers", - "Loading...": "Loading...", + "Loading {{label}}": "Loading {{label}}", + "Loading execution details": "Loading execution details", + "Loading logs...": "Loading logs...", + "Loading remediation options": "Loading remediation options", + "Loading root cause": "Loading root cause", + "Loading root cause analysis": "Loading root cause analysis", "Manual": "Manual", "Max retry attempts": "Max retry attempts", "Max Turns": "Max Turns", "Model": "Model", "Name": "Name", "Namespace": "Namespace", - "Namespace Scoped": "Namespace Scoped", - "Needs approval": "Needs approval", "No agents found.": "No agents found.", - "No escalation result yet.": "No escalation result yet.", - "No execution result yet.": "No execution result yet.", "No LLM providers found.": "No LLM providers found.", - "No options proposed": "No options proposed", - "No proposal generated yet.": "No proposal generated yet.", + "No logs available.": "No logs available.", "No proposals": "No proposals", "No proposals match the current filters. Try adjusting your filters.": "No proposals match the current filters. Try adjusting your filters.", "No providers available": "No providers available", "No queries or series data provided": "No queries or series data provided", + "No remediation options were generated by the analysis.": "No remediation options were generated by the analysis.", "No results found": "No results found", - "No verification result yet.": "No verification result yet.", - "Not reversible": "Not reversible", + "Option {{number}}": "Option {{number}}", "Optional": "Optional", - "Overview": "Overview", - "Pass": "Pass", + "Original root cause": "Original root cause", "Passed": "Passed", - "Phase": "Phase", - "Pod starting": "Pod starting", "Project ID": "Project ID", "PromQL": "PromQL", "Proposal": "Proposal", - "Proposal not found": "Proposal not found", "Proposals are created by adapters or by user request. No proposals have been created yet.": "Proposals are created by adapters or by user request. No proposals have been created yet.", - "Proposed Remediation": "Proposed Remediation", + "Proposed actions": "Proposed actions", "Provider Type": "Provider Type", "Query NOT tested": "Query NOT tested", "Query tested": "Query tested", - "Re-analyzing with feedback...": "Re-analyzing with feedback...", - "Reconnecting...": "Reconnecting...", - "Refine": "Refine", - "Refine failed": "Refine failed", "Region": "Region", - "Request": "Request", - "Required RBAC Permissions": "Required RBAC Permissions", - "Resource": "Resource", - "Resource Names": "Resource Names", - "Resources": "Resources", - "Reversible": "Reversible", - "Review before approving": "Review before approving", - "Review the advisory below and apply changes externally (e.g., via GitOps). No direct cluster execution will occur.": "Review the advisory below and apply changes externally (e.g., via GitOps). No direct cluster execution will occur.", - "Review these permissions carefully before approving. This is the exact set of permissions the Lightspeed operator will grant to the agent's execution sandbox. These permissions are enforced on every iteration, including retries, and cannot be altered by the agent during execution.": "Review these permissions carefully before approving. This is the exact set of permissions the Lightspeed operator will grant to the agent's execution sandbox. These permissions are enforced on every iteration, including retries, and cannot be altered by the agent during execution.", - "Revision feedback": "Revision feedback", - "Risk: {{risk}}": "Risk: {{risk}}", - "Rollback Plan": "Rollback Plan", + "Remediation delta": "Remediation delta", + "Remediation hub": "Remediation hub", + "Remediation options will appear once analysis is complete.": "Remediation options will appear once analysis is complete.", + "Restricted access": "Restricted access", + "Resume auto-scroll": "Resume auto-scroll", + "Rollback plan": "Rollback plan", "Root Cause": "Root Cause", + "Root cause analysis (RCA)": "Root cause analysis (RCA)", + "Root cause analysis was not completed.": "Root cause analysis was not completed.", "Save": "Save", - "Searching for pod": "Searching for pod", - "Select agent": "Select agent", + "Search logs...": "Search logs...", "Select this approach": "Select this approach", - "Select this option": "Select this option", - "Selected": "Selected", - "Show analysis logs": "Show analysis logs", - "Show escalation logs": "Show escalation logs", - "Show execution logs": "Show execution logs", + "Selected option": "Selected option", + "Show {{title}} logs": "Show {{title}} logs", "Show Timeouts": "Show Timeouts", - "Show verification logs": "Show verification logs", - "Source": "Source", "Step {{order}}": "Step {{order}}", - "Stream ended": "Stream ended", - "Submit": "Submit", - "Summary": "Summary", - "Target Namespaces": "Target Namespaces", "Test Result": "Test Result", - "The agent did not return any trigger proposals.": "The agent did not return any trigger proposals.", - "These actions reflect the agent's planned approach based on its analysis. During execution, the agent may follow this plan exactly or choose a more optimal path. The RBAC permissions below are locked at approval time and enforced by the Lightspeed operator on every execution attempt. The agent cannot escalate its own privileges.": "These actions reflect the agent's planned approach based on its analysis. During execution, the agent may follow this plan exactly or choose a more optimal path. The RBAC permissions below are locked at approval time and enforced by the Lightspeed operator on every execution attempt. The agent cannot escalate its own privileges.", - "This proposal requires approval before analysis can begin.": "This proposal requires approval before analysis can begin.", + "This is an advisory-only proposal. Review the recommendations below and apply changes externally.": "This is an advisory-only proposal. Review the recommendations below and apply changes externally.", + "Timeline": "Timeline", + "Trigger domain": "Trigger domain", "Type": "Type", - "Unknown": "Unknown", + "Unable to load {{label}}": "Unable to load {{label}}", + "Unable to load analysis results.": "Unable to load analysis results.", "URL Override": "URL Override", "Value:": "Value:", - "Verbs": "Verbs", "Verification": "Verification", "Verification (seconds)": "Verification (seconds)", - "Verification Plan": "Verification Plan", - "Verification requires approval before it can proceed.": "Verification requires approval before it can proceed.", - "Verification Result": "Verification Result", - "Verification Sandbox": "Verification Sandbox", - "Verification Steps": "Verification Steps", + "Verification checks": "Verification checks", + "Verification steps": "Verification steps", + "Verification summary": "Verification summary", "Verify:": "Verify:", - "Verifying...": "Verifying...", "View {{name}}": "View {{name}}", - "Waiting for escalation sandbox...": "Waiting for escalation sandbox...", - "Waiting for execution sandbox...": "Waiting for execution sandbox...", - "Waiting for sandbox pod...": "Waiting for sandbox pod...", - "Waiting for verification sandbox...": "Waiting for verification sandbox...", - "You must be a member of system:cluster-admins to approve or deny proposals.": "You must be a member of system:cluster-admins to approve or deny proposals.", - "Your feedback": "Your feedback" -} \ No newline at end of file + "Waiting for analysis to start...": "Waiting for analysis to start...", + "You don't have permission to view this {{label}}.": "You don't have permission to view this {{label}}." +} diff --git a/package-lock.json b/package-lock.json index 1d37485..fbf45ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,10 @@ "@openshift-console/dynamic-plugin-sdk": "4.22.0", "@openshift-console/dynamic-plugin-sdk-webpack": "4.22.0", "@patternfly/react-charts": "^8.4.1", + "@patternfly/react-component-groups": "^6.5.0", "@patternfly/react-core": "^6.4.3", "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-log-viewer": "^6.5.0", "@patternfly/react-table": "^6.4.3", "@types/react": "^18.3.28", "copy-webpack-plugin": "14.0.0", @@ -601,6 +603,93 @@ "node": ">=14.17.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz", + "integrity": "sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "0.7.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz", + "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -2127,15 +2216,33 @@ } } }, + "node_modules/@patternfly/react-component-groups": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-6.5.0.tgz", + "integrity": "sha512-uvrAuUixZW618gsQY06nOcQWZclsCtOa5mixi6KQKOyrpH9GgtrihJ/fVJsA1a5HcZzC4SKIdd3wkXArs/vxEg==", + "license": "MIT", + "dependencies": { + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", + "@patternfly/react-table": "^6.0.0", + "react-jss": "^10.10.0" + }, + "peerDependencies": { + "@patternfly/react-drag-drop": "^6.0.0", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-core": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.5.1.tgz", - "integrity": "sha512-fFZ0hcIyHJO27hxbf53W3m2R11l0O9WxR7CusJXuCEaNMP31ULrhf5Pv6ROdTrrs39Kl/yPv+2QuxQfe/4e72g==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.6.0.tgz", + "integrity": "sha512-MLYPcJvdDWHZqWcvEH2QhKtCg6APRfrDJ4o192/wc2LUaf4ViGVsVLKDMbaKCamhbGauv7KpffNCSy3fjRy4Bw==", "license": "MIT", "dependencies": { - "@patternfly/react-icons": "^6.5.1", - "@patternfly/react-styles": "^6.5.1", - "@patternfly/react-tokens": "^6.5.1", + "@patternfly/react-icons": "^6.6.0", + "@patternfly/react-styles": "^6.6.0", + "@patternfly/react-tokens": "^6.6.0", "focus-trap": "7.6.6", "react-dropzone": "^14.3.5", "tslib": "^2.8.1" @@ -2145,20 +2252,59 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-drag-drop": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.6.0.tgz", + "integrity": "sha512-RWVUhK/CtT2ak7ZRypLRcX41dhSUK1zcOG0FvNcd2A2Do134VNxwVWCVKC+8ff4AsQcIG97IAO4+uwHx8PXDlA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@patternfly/react-core": "^6.6.0", + "@patternfly/react-icons": "^6.6.0", + "@patternfly/react-styles": "^6.6.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.5.1.tgz", - "integrity": "sha512-CnuPvTTs4MMWx8CAUkmnY690ouN1bbHjunsyXu3QxvGOmzbztP+wS4BdiLS8TIXOIH80Yb7KPhnF8VkA+CduOA==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.6.0.tgz", + "integrity": "sha512-cDDUm4PFxqeBo4PS08xO1eFd6CMTOppn5RS4fs2hAgu/vRFkPXuL0zZCkxgRg5asd/W4olEN7WrIAbvjKbHKiQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, + "node_modules/@patternfly/react-log-viewer": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-log-viewer/-/react-log-viewer-6.5.0.tgz", + "integrity": "sha512-oLsqTKev5jq05vL8w3hplW1Iov0AYWzF/RV1qPhcTM7IUKsBUTGk/xO4rcDCkoPkLAeBbmKAH9uLQeLhnzOkaw==", "license": "MIT", + "dependencies": { + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", + "memoize-one": "^5.1.0" + }, "peerDependencies": { "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" } }, "node_modules/@patternfly/react-styles": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.5.1.tgz", - "integrity": "sha512-yQMzUbbf6qYM/v3JbPvaCJTgxRbOKoEw229XZmnnM8gDvp8ECiI7LqihrAOK/NA6T6M3DDgsRMd2JurUBhPDEw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.6.0.tgz", + "integrity": "sha512-u/HzJ48Cg6Nb12dIVbj4g31lMDVHPHVKRXkRuPBcuK6MQyAsxSfcyWVivW7Lga8TsNhFW26JWB5ay+IW5xpZ8A==", "license": "MIT" }, "node_modules/@patternfly/react-table": { @@ -2180,9 +2326,9 @@ } }, "node_modules/@patternfly/react-tokens": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.5.1.tgz", - "integrity": "sha512-zwepLsIQTL0Lf4R2/PIBOk1m+pm0hYVT3lktf2H4+Y87eRIifwMRb19c+pr4hj4ckGvHs+WxwjTfTj2Qqwn5rw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.6.0.tgz", + "integrity": "sha512-oRUFYJAM9LKYTM9YRnd7rz3wB6ZoaBWit/Eu+EK22J4o5AhZRjLt0+ewXQ7I99dBP4c8BbnPEUFnxQYAroqNLg==", "license": "MIT" }, "node_modules/@peculiar/asn1-cms": { @@ -5050,6 +5196,17 @@ "node": ">=12" } }, + "node_modules/css-jss": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/css-jss/-/css-jss-10.10.0.tgz", + "integrity": "sha512-YyMIS/LsSKEGXEaVJdjonWe18p4vXLo8CMA4FrW/kcaEyqdIGKCFXao31gbJddXEdIxSXFFURWrenBJPlKTgAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "^10.10.0", + "jss-preset-default": "^10.10.0" + } + }, "node_modules/css-loader": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", @@ -5128,6 +5285,16 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, "node_modules/css-what": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", @@ -7011,6 +7178,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7795,6 +7963,12 @@ "node": ">=10.18" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, "node_modules/i18next": { "version": "26.3.2", "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.2.tgz", @@ -8315,6 +8489,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==", + "license": "MIT" + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -8825,6 +9005,172 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jss": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", + "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/jss" + } + }, + "node_modules/jss-plugin-camel-case": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz", + "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-compose": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-compose/-/jss-plugin-compose-10.10.0.tgz", + "integrity": "sha512-F5kgtWpI2XfZ3Z8eP78tZEYFdgTIbpA/TMuX3a8vwrNolYtN1N4qJR/Ob0LAsqIwCMLojtxN7c7Oo/+Vz6THow==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-default-unit": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz", + "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-expand": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-expand/-/jss-plugin-expand-10.10.0.tgz", + "integrity": "sha512-ymT62W2OyDxBxr7A6JR87vVX9vTq2ep5jZLIdUSusfBIEENLdkkc0lL/Xaq8W9s3opUq7R0sZQpzRWELrfVYzA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-extend": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-extend/-/jss-plugin-extend-10.10.0.tgz", + "integrity": "sha512-sKYrcMfr4xxigmIwqTjxNcHwXJIfvhvjTNxF+Tbc1NmNdyspGW47Ey6sGH8BcQ4FFQhLXctpWCQSpDwdNmXSwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-global": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz", + "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-nested": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz", + "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-props-sort": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz", + "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-rule-value-function": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz", + "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-rule-value-observable": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-observable/-/jss-plugin-rule-value-observable-10.10.0.tgz", + "integrity": "sha512-ZLMaYrR3QE+vD7nl3oNXuj79VZl9Kp8/u6A1IbTPDcuOu8b56cFdWRZNZ0vNr8jHewooEeq2doy8Oxtymr2ZPA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "symbol-observable": "^1.2.0" + } + }, + "node_modules/jss-plugin-template": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-template/-/jss-plugin-template-10.10.0.tgz", + "integrity": "sha512-ocXZBIOJOA+jISPdsgkTs8wwpK6UbsvtZK5JI7VUggTD6LWKbtoxUzadd2TpfF+lEtlhUmMsCkTRNkITdPKa6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-vendor-prefixer": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz", + "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.10.0" + } + }, + "node_modules/jss-preset-default": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-preset-default/-/jss-preset-default-10.10.0.tgz", + "integrity": "sha512-GL175Wt2FGhjE+f+Y3aWh+JioL06/QWFgZp53CbNNq6ZkVU0TDplD8Bxm9KnkotAYn3FlplNqoW5CjyLXcoJ7Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "jss-plugin-camel-case": "10.10.0", + "jss-plugin-compose": "10.10.0", + "jss-plugin-default-unit": "10.10.0", + "jss-plugin-expand": "10.10.0", + "jss-plugin-extend": "10.10.0", + "jss-plugin-global": "10.10.0", + "jss-plugin-nested": "10.10.0", + "jss-plugin-props-sort": "10.10.0", + "jss-plugin-rule-value-function": "10.10.0", + "jss-plugin-rule-value-observable": "10.10.0", + "jss-plugin-template": "10.10.0", + "jss-plugin-vendor-prefixer": "10.10.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -9108,6 +9454,12 @@ "tslib": "2" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, "node_modules/meow": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", @@ -10434,6 +10786,12 @@ "node": ">=0.10.0" } }, + "node_modules/react-display-name": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz", + "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==", + "license": "MIT" + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -10503,6 +10861,28 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-jss": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/react-jss/-/react-jss-10.10.0.tgz", + "integrity": "sha512-WLiq84UYWqNBF6579/uprcIUnM1TSywYq6AIjKTTTG5ziJl9Uy+pwuvpN3apuyVwflMbD60PraeTKT7uWH9XEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "@emotion/is-prop-valid": "^0.7.3", + "css-jss": "10.10.0", + "hoist-non-react-statics": "^3.2.0", + "is-in-browser": "^1.1.3", + "jss": "10.10.0", + "jss-preset-default": "10.10.0", + "prop-types": "^15.6.0", + "shallow-equal": "^1.2.0", + "theming": "^3.3.0", + "tiny-warning": "^1.0.2" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, "node_modules/react-router": { "version": "7.13.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", @@ -10681,6 +11061,13 @@ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", "license": "MIT" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT", + "peer": true + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -11296,6 +11683,12 @@ "node": ">=8" } }, + "node_modules/shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12243,6 +12636,15 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -12502,6 +12904,24 @@ } } }, + "node_modules/theming": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/theming/-/theming-3.3.0.tgz", + "integrity": "sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0", + "prop-types": "^15.5.8", + "react-display-name": "^0.2.4", + "tiny-warning": "^1.0.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, "node_modules/thingies": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", @@ -12543,6 +12963,12 @@ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", "license": "MIT" }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/package.json b/package.json index 6482df1..44fa2ec 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,10 @@ "@openshift-console/dynamic-plugin-sdk": "4.22.0", "@openshift-console/dynamic-plugin-sdk-webpack": "4.22.0", "@patternfly/react-charts": "^8.4.1", + "@patternfly/react-component-groups": "^6.5.0", "@patternfly/react-core": "^6.4.3", "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-log-viewer": "^6.5.0", "@patternfly/react-table": "^6.4.3", "@types/react": "^18.3.28", "copy-webpack-plugin": "14.0.0", diff --git a/src/components/ConfirmationModal.tsx b/src/components/ConfirmationModal.tsx new file mode 100644 index 0000000..12fe915 --- /dev/null +++ b/src/components/ConfirmationModal.tsx @@ -0,0 +1,60 @@ +import type { FC } from 'react'; +import { + Alert, + Button, + Content, + ContentVariants, + Modal, + ModalBody, + ModalFooter, + ModalHeader, +} from '@patternfly/react-core'; +import { useTranslation } from 'react-i18next'; + +export interface ConfirmationModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + body: string; + actionLabel: string; + actionVariant: 'primary' | 'danger'; + onAction: () => void | Promise; + isLoading: boolean; + error?: string; +} + +export const ConfirmationModal: FC = ({ + isOpen, + onClose, + title, + body, + actionLabel, + actionVariant, + onAction, + isLoading, + error, +}) => { + const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); + return ( + + + + {body} + {error && } + + + + + + + ); +}; diff --git a/src/components/StatusGuard.tsx b/src/components/StatusGuard.tsx new file mode 100644 index 0000000..c719821 --- /dev/null +++ b/src/components/StatusGuard.tsx @@ -0,0 +1,57 @@ +import type { FC, ReactNode } from 'react'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import { ErrorState } from '@patternfly/react-component-groups'; +import { HttpError } from '@openshift-console/dynamic-plugin-sdk'; +import { useTranslation } from 'react-i18next'; + +interface StatusGuardProps { + data: unknown; + loaded: boolean; + loadError: Error | undefined; + label: string; + children: ReactNode; +} + +const getErrorCode = (error: Error): number | undefined => + error instanceof HttpError ? error.code : undefined; + +const StatusGuard: FC = ({ data, loaded, loadError, label, children }) => { + const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); + + if (loadError) { + const code = getErrorCode(loadError); + if (code === 403) { + return ( + + ); + } + if (code === 404) { + return ; + } + return ( + + ); + } + + if (!loaded) { + return ( + + + + ); + } + + if (!data) { + return ; + } + + return <>{children}; +}; + +export default StatusGuard; diff --git a/src/components/proposals/DynamicComponent.tsx b/src/components/proposals/DynamicComponent.tsx deleted file mode 100644 index d609d5c..0000000 --- a/src/components/proposals/DynamicComponent.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './dynamic'; -export type { DynamicComponentProps } from './dynamic/types'; diff --git a/src/components/proposals/EscalateModal.tsx b/src/components/proposals/EscalateModal.tsx deleted file mode 100644 index 2d77c82..0000000 --- a/src/components/proposals/EscalateModal.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Alert, - Button, - Modal, - ModalBody, - ModalFooter, - ModalHeader, - ModalVariant, - Tooltip, -} from '@patternfly/react-core'; - -import { LightspeedProposal, LightspeedProposalApproval } from '../../models/proposal'; -import { useStageApproval } from '../../hooks/useStageApproval'; - -const EscalateModal: React.FC<{ - isOpen: boolean; - onClose: () => void; - proposal: LightspeedProposal; - approval?: LightspeedProposalApproval; -}> = ({ isOpen, onClose, proposal, approval }) => { - const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); - const escalation = useStageApproval(proposal, approval, 'Escalation'); - - const onApprove = React.useCallback(async () => { - if (!escalation.canApprove) return; - await escalation.approve(); - if (!escalation.error) { - onClose(); - } - }, [escalation, onClose]); - - return ( - - - -

- {t( - 'Approve the escalation step for proposal {{name}}. The agent will research the issue, draft a support case, and file it.', - { name: proposal.metadata.name }, - )} -

- {escalation.error && ( - <> -
- - {escalation.error} - - - )} -
- - - - - - -
- ); -}; - -export default EscalateModal; diff --git a/src/components/proposals/MarkdownText.tsx b/src/components/proposals/MarkdownText.tsx deleted file mode 100644 index 68b8213..0000000 --- a/src/components/proposals/MarkdownText.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react'; -import { renderMarkdown } from '../../utils/markdown'; - -export const MarkdownText: React.FC<{ content: string }> = ({ content }) => { - const html = React.useMemo(() => renderMarkdown(content), [content]); - return
; -}; diff --git a/src/components/proposals/PhaseIcon.tsx b/src/components/proposals/PhaseIcon.tsx deleted file mode 100644 index 15f0eda..0000000 --- a/src/components/proposals/PhaseIcon.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; -import { - BanIcon, - CheckCircleIcon, - ExclamationCircleIcon, - ExclamationTriangleIcon, - HourglassStartIcon, - InProgressIcon, - SearchIcon, - TimesCircleIcon, -} from '@patternfly/react-icons'; - -import { ProposalPhase } from '../../models/proposal'; - -const PhaseIcon: React.FC<{ - phase?: ProposalPhase | string; - executionFailed?: boolean; - verificationFailed?: boolean; -}> = ({ phase, executionFailed, verificationFailed }) => { - switch (phase) { - case 'Pending': - return ; - case 'Analyzing': - return ; - case 'Executing': - return ; - case 'Completed': { - if (executionFailed || verificationFailed) { - return ( - - ); - } - return ( - - ); - } - case 'Failed': - return ( - - ); - case 'Denied': - return ( - - ); - case 'Escalated': - return ( - - ); - case 'Escalating': - return ( - - ); - case 'EmergencyStopped': - return ; - default: - return ; - } -}; - -export default PhaseIcon; diff --git a/src/components/proposals/ProposalDetailPage.tsx b/src/components/proposals/ProposalDetailPage.tsx index 7516b91..b1d8500 100644 --- a/src/components/proposals/ProposalDetailPage.tsx +++ b/src/components/proposals/ProposalDetailPage.tsx @@ -1,2148 +1,396 @@ -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router'; -import { - K8sResourceCommon, - k8sPatch, - useK8sWatchResource, -} from '@openshift-console/dynamic-plugin-sdk'; +import { useState, useCallback, useEffect } from 'react'; + +import type { FC, ReactNode } from 'react'; import { Alert, - Button, + Breadcrumb, + BreadcrumbItem, Card, CardBody, - CardTitle, - CodeBlock, - CodeBlockCode, - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - Dropdown, - DropdownItem, - DropdownList, - ExpandableSection, + Content, + ContentVariants, + Divider, + EmptyState, + EmptyStateBody, Flex, FlexItem, - MenuToggle, - MenuToggleAction, Label, + PageGroup, PageSection, - Stack, - StackItem, - TextArea, + Skeleton, Title, - Tooltip, } from '@patternfly/react-core'; - -import { - CheckCircleIcon, - ExclamationCircleIcon, - ExternalLinkAltIcon, -} from '@patternfly/react-icons'; - -import { - AdapterComponent, - AnalysisResultCR, - AnalysisResultGVK, - derivePhaseFromConditions, - EscalationResultCR, - EscalationResultGVK, - ExecutionResultCR, - ExecutionResultGVK, - getPhaseDisplay, - getRiskColor, - LightspeedAgentModel, - LightspeedProposal, - LightspeedProposalApproval, - LightspeedProposalApprovalModel, - LightspeedProposalModel, - PermissionRule, - ProposalCondition, - RemediationOption, - resultOutcome, - SandboxInfo, - StepResultRef, - VerificationResultCR, - VerificationResultGVK, -} from '../../models/proposal'; -import { type StageApprovalResult, useStageApproval } from '../../hooks/useStageApproval'; -import { MarkdownText } from './MarkdownText'; -import DynamicComponent from './DynamicComponent'; -import EscalateModal from './EscalateModal'; +import { DocumentTitle, ResourceIcon, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router'; +import { useProposal } from '../../hooks/useProposal'; +import { LightspeedProposalGVK } from '../../models/proposal'; +import type { ProposalView } from '../../models/proposal-views'; +import { TERMINAL_PHASES } from '../../models/proposal-views'; +import { ProposalPhaseLabel } from './detail/ProposalPhaseLabel'; +import { AnalysisSummary } from './detail/AnalysisSummary'; +import { RemediationOptionCard } from './detail/RemediationOptionCard'; +import { ExecutionSummary } from './detail/ExecutionSummary'; +import { VerificationSummary } from './detail/VerificationSummary'; +import { StageInProgress } from './detail/StageInProgress'; +import { ProposalTimeline } from './detail/ProposalTimeline'; +import { ConfirmationModal } from '../ConfirmationModal'; +import StatusGuard from '../StatusGuard'; import AgenticLayout from '../AgenticLayout'; -import PhaseIcon from './PhaseIcon'; -import SandboxLogViewer from './SandboxLogViewer'; - -import './proposal-detail.css'; - -const KNOWN_COMPONENT_TYPES = new Set([ - 'lightspeed_prometheus_query', - 'lightspeed_metrics_chart', - 'lightspeed_resource_diff', - 'lightspeed_action_picker', - 'lightspeed_evidence_table', - 'lightspeed_status_timeline', - 'cmo_alert_diagnosis', - 'cmo_metric_evidence', - 'cmo_remediation_step', - 'cmo_trigger_proposal', -]); - -const AdapterComponents: React.FC<{ components?: unknown }> = ({ components }) => { - if (!components) { - return null; - } - const items: AdapterComponent[] = Array.isArray(components) - ? components - : [components as AdapterComponent]; - return ( - - {items.map((comp, i) => ( - - {typeof comp === 'object' && - comp !== null && - 'type' in comp && - KNOWN_COMPONENT_TYPES.has((comp as AdapterComponent).type) ? ( - - ) : ( - - - - {JSON.stringify(comp, null, 2)} - - - - )} - - ))} - - ); -}; - -/** Auto-collapse a log viewer section once data arrives for the first time. */ -function useAutoCollapseLogs(hasData: boolean): [boolean, (_v: boolean) => void] { - const [expanded, setExpanded] = React.useState(true); - const prevRef = React.useRef(hasData); - React.useEffect(() => { - if (hasData && !prevRef.current) { - setExpanded(false); - } - prevRef.current = hasData; - }, [hasData]); - return [expanded, setExpanded]; -} - -const CONFIRM_RESET_MS = 5000; -const MAX_RETRIES = 3; -const RETRY_OPTIONS = Array.from({ length: MAX_RETRIES }, (_, i) => i + 1); - -const AgentDropdown: React.FC<{ - agentNames: string[]; - defaultAgent?: string; - selected: string; - onSelect: (_name: string) => void; -}> = ({ agentNames, defaultAgent, selected, onSelect }) => { - const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); - const [isOpen, setIsOpen] = React.useState(false); - let displayLabel: string; - if (!selected) { - displayLabel = t('Select agent'); - } else if (selected === defaultAgent) { - displayLabel = `${selected} (${t('default')})`; - } else { - displayLabel = selected; - } - - return ( - { - onSelect(value as string); - setIsOpen(false); - }} - toggle={(toggleRef) => ( - setIsOpen((o) => !o)} ref={toggleRef} variant="secondary"> - {displayLabel} - - )} - > - - {agentNames.map((name) => ( - - {name === defaultAgent ? `${name} (${t('default')})` : name} - - ))} - - - ); -}; -const ApprovalCard: React.FC<{ - approval: StageApprovalResult; - approveLabel: string; - message: string; - defaultAgent?: string; - agentNames: string[]; -}> = ({ approval, approveLabel, message, defaultAgent, agentNames }) => { +const ProposalDetailsPage: FC = () => { const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); - const [selectedAgent, setSelectedAgent] = React.useState(defaultAgent ?? ''); - - return ( - - - - {t('Approval Required')} - - - {message} - - - {agentNames.length > 0 && ( - - - - )} - - - - - - - - - - - - - {approval.error && ( - - - {approval.error} - - - )} - - - - - - ); -}; - -const SandboxDisplay: React.FC<{ label: string; sandbox?: SandboxInfo }> = ({ label, sandbox }) => { - if (!sandbox?.claimName) { - return null; - } - return ( - - {label} - - {sandbox.claimName} - {sandbox.namespace && ` (${sandbox.namespace})`} - - - ); -}; - -const OverviewTab: React.FC<{ - proposal: LightspeedProposal; - approval?: LightspeedProposalApproval; - latestExecutionResult?: ExecutionResultCR; - latestVerificationResult?: VerificationResultCR; -}> = ({ proposal, approval, latestExecutionResult, latestVerificationResult }) => { - const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); - const phase = getPhaseDisplay( - derivePhaseFromConditions(proposal.status?.conditions as ProposalCondition[]), - ); - const sourceUrl = proposal.metadata.annotations?.['ols.openshift.io/source-url']; - const sourceName = proposal.metadata.annotations?.['ols.openshift.io/source-name'] || t('Source'); - const execFailed = resultOutcome(latestExecutionResult) === 'Failed'; - const verifyFailed = resultOutcome(latestVerificationResult) === 'Failed'; - - return ( - - - - {t('Details')} - - - - {t('Phase')} - - - - - - - - - - - - - {t('Request')} - - - - - {sourceUrl && ( - - {t('Source')} - - - - - )} - {proposal.spec.targetNamespaces && proposal.spec.targetNamespaces.length > 0 && ( - - {t('Target Namespaces')} - - {proposal.spec.targetNamespaces.join(', ')} - - - )} - - {t('Created')} - - {proposal.metadata.creationTimestamp - ? new Date(proposal.metadata.creationTimestamp).toLocaleString() - : '-'} - - - {approval?.spec?.approver?.username && ( - - {t('Approved By')} - - {approval.spec.approver.username} - {approval.spec.approver.approvedAt && - ` (${new Date(approval.spec.approver.approvedAt).toLocaleString()})`} - - - )} - - - - - - - - - ); -}; + const navigate = useNavigate(); + const params = useParams<{ ns: string; name: string }>(); + const name = params.name ?? ''; + const namespace = params.ns; + + const { + proposal, + view, + proposalLoaded, + proposalError, + resultsLoaded, + resultsError, + approveExecution, + mutationInProgress, + mutationError, + clearMutationError, + } = useProposal(name, namespace); + + const phaseKey = view?.phase ?? 'unknown'; + const [selectedOption, setSelectedOption] = useState(0); + const [expandedOption, setExpandedOption] = useState(0); + + const executedOptionIndex = view?.executedOptionIndex; + useEffect(() => { + const idx = executedOptionIndex ?? 0; + // eslint-disable-next-line react-hooks/set-state-in-effect + setSelectedOption(idx); -const confidenceColor = (c: string): 'green' | 'orange' | 'red' | 'grey' => { - switch (c) { - case 'high': - return 'green'; - case 'medium': - return 'orange'; - case 'low': - return 'red'; - default: - return 'grey'; - } -}; + setExpandedOption(idx); + }, [phaseKey, executedOptionIndex]); -const stepTypeLabel = (type: string): string => { - switch (type) { - case 'command': - return 'CLI'; - case 'metric': - return 'Metric'; - case 'resource_check': - return 'Resource'; - case 'log_check': - return 'Log'; - case 'api_call': - return 'API'; - default: - return type; - } -}; + const selectOption = useCallback((idx: number) => { + setSelectedOption(idx); + setExpandedOption((prev) => (prev === idx ? -1 : idx)); + }, []); -const PermissionRulesTable: React.FC<{ rules: PermissionRule[] }> = ({ rules }) => { - const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); - if (!rules || rules.length === 0) { - return null; - } - return ( - - {rules.map((rule, i) => ( - - - - - {rule.namespace && ( - - {t('Namespace')} - - - - - )} - - {t('API Groups')} - - {rule.apiGroups.map((g) => ( - - ))} - - - - {t('Resources')} - - {rule.resources.map((r) => ( - - ))} - - - {rule.resourceNames && rule.resourceNames.length > 0 && ( - - {t('Resource Names')} - - {rule.resourceNames.map((n) => ( - - ))} - - - )} - - {t('Verbs')} - - {rule.verbs.map((v) => ( - - ))} - - - - {t('Justification')} - {rule.justification} - - - - - - ))} - - ); -}; + const [executeOptionIndex, setExecuteOptionIndex] = useState(null); -const RemediationOptionCard: React.FC<{ option: RemediationOption }> = ({ option }) => { - const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); - const { diagnosis, proposal, rbac, verification } = option; - const [highlight, setHighlight] = React.useState(false); - const serialized = JSON.stringify(option); - const prevRef = React.useRef(serialized); + const openExecuteModal = useCallback(() => { + setExecuteOptionIndex(selectedOption); + }, [selectedOption]); - React.useEffect(() => { - if (prevRef.current === serialized) { - return; - } - prevRef.current = serialized; - setHighlight(true); - const timer = setTimeout(() => setHighlight(false), 1500); - return () => clearTimeout(timer); - }, [serialized]); + const handleApproveExecution = useCallback(async () => { + if (executeOptionIndex === null) return; + const success = await approveExecution(executeOptionIndex); + if (success) setExecuteOptionIndex(null); + }, [approveExecution, executeOptionIndex]); - return ( - - {diagnosis && ( - + const renderRemediationHub = (v: ProposalView): ReactNode => { + switch (v.phase) { + case 'Pending': + case 'Analyzing': + return ( - {t('Diagnosis')} - - - {t('Root Cause')} - - - - - - {t('Summary')} - - - - - - {t('Confidence')} - - - - - - - - - )} - - {proposal && ( - - - - - {t('Proposed Remediation')} + - + + {t('Remediation options will appear once analysis is complete.')} + - + - - - - - - - - {t( - "These actions reflect the agent's planned approach based on its analysis. During execution, the agent may follow this plan exactly or choose a more optimal path. The RBAC permissions below are locked at approval time and enforced by the Lightspeed operator on every execution attempt. The agent cannot escalate its own privileges.", - )} - - - - - - - {proposal.actions?.length > 0 && ( - - {t('Actions')} - - {proposal.actions.map((action, i) => ( - - - - - - - - - {action.description} - - - - - - ))} - - - )} - - - - - )} - - {rbac && (rbac.namespaceScoped?.length > 0 || rbac.clusterScoped?.length > 0) && ( - - - {t('Required RBAC Permissions')} - - - - - - {t( - "Review these permissions carefully before approving. This is the exact set of permissions the Lightspeed operator will grant to the agent's execution sandbox. These permissions are enforced on every iteration, including retries, and cannot be altered by the agent during execution.", - )} - - - - {rbac.namespaceScoped?.length > 0 && ( - - {t('Namespace Scoped')} - - - )} - {rbac.clusterScoped?.length > 0 && ( - - {t('Cluster Scoped')} - - - )} - - - - - )} - - {verification && verification.steps?.length > 0 && ( - - - {t('Verification Plan')} - - - - - - {t( - "After execution, a separate verification agent with read-only cluster access independently checks whether the fix worked. It inspects actual cluster state, not the execution agent's self-reported results. The checks shown below are the analysis agent's recommendations. The verification agent uses its own judgment based on what it observes, and every check it runs is reported transparently in the Verification tab. If verification fails, and retries were selected at approval, the Lightspeed operator will automatically retry execution with the failure reasons included as context for the execution agent.", - )} - - - - - - - - {t('Verification Steps')} - - {verification.steps.map((step, i) => ( - - - - - - - - - - - {step.name} - - - - - - {step.command} - - - - - - {t('Expected')} - - {step.expected} - - - - - - - - - ))} - - - {proposal?.rollbackPlan && ( - - {t('Rollback Plan')} - {typeof proposal.rollbackPlan === 'object' ? ( - - - - - - - {proposal.rollbackPlan.command} - - - - ) : ( - - {proposal.rollbackPlan} - - )} - - )} - - - - - )} - - - - ); -}; - -const RemediationOptionsView: React.FC<{ - options: RemediationOption[]; - selectedIndex?: number; - onSelect?: (_index: number) => void; - actionButtons?: React.ReactNode; -}> = ({ options, selectedIndex, onSelect, actionButtons }) => { - const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); - - if (options.length === 1) { - return ( - - - {actionButtons &&
{actionButtons}
} -
- ); - } - - return ( - - {options.map((opt, i) => { - const isSelected = selectedIndex === i; - return ( - - - - - {opt.title} - - {opt.proposal && ( - - - - )} - {opt.summary && {opt.summary}} - {isSelected && ( - - - - )} - - } - > - - {onSelect && !isSelected && ( - - )} - {isSelected && actionButtons && ( -
{actionButtons}
- )} -
-
-
- ); - })} -
- ); -}; - -const StructuredResult: React.FC<{ data: ExecutionResultCR }> = ({ data }) => { - const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); - const executionOutcome = resultOutcome(data); - - return ( - - - - - - - {executionOutcome === 'Succeeded' ? ( - - ) : ( - - )} - - - {executionOutcome === 'Succeeded' - ? t('Execution Succeeded') - : t('Execution Failed')} - - - - - - - {(data.status?.actionsTaken?.length ?? 0) > 0 && ( - - - {t('Actions Taken')} - - - {data.status?.actionsTaken!.map((action, i) => ( - - - - - - - - {action.outcome === 'Succeeded' ? ( - - ) : ( - - )} - - - - - {action.description} - - - {action.resource && ( - - - - {t('Resource')} - - {action.resource.kind} - {action.resource.namespace - ? ` ${action.resource.namespace}/` - : ' '} - {action.resource.name} - - - - - )} - {action.output && ( - - - {action.output} - - - )} - {action.error && ( - - - {action.error} - - - )} - - - - - ))} - - - - - )} - - {data.status?.verification && ( - - - - - {data.status?.verification.conditionOutcome === 'Improved' ? ( - - ) : data.status?.verification.conditionOutcome === 'Degraded' ? ( - - ) : ( - - )} + - {data.status?.verification.conditionOutcome === 'Improved' - ? t('Condition Improved') - : data.status?.verification.conditionOutcome === 'Degraded' - ? t('Condition Degraded') - : t('Condition Unchanged')} + - - - - - )} - - ); -}; - -interface ProposalTabProps { - proposal: LightspeedProposal; - analysisApproval: StageApprovalResult; - executionApproval: StageApprovalResult; - agentNames: string[]; - latestAnalysisResult?: AnalysisResultCR; -} - -const ProposalTab: React.FC = ({ - proposal, - analysisApproval, - executionApproval, - agentNames, - latestAnalysisResult, -}) => { - const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin'); - const analysis = proposal.status?.steps?.analysis; - const options = latestAnalysisResult?.status?.options ?? []; - const hasAnalysis = options.length > 0; - const phase = derivePhaseFromConditions(proposal.status?.conditions as ProposalCondition[]); - const showExecutionApproval = executionApproval.needsApproval && hasAnalysis; - const [confirmRetries, setConfirmRetries] = React.useState(null); - const [retryDropdownOpen, setRetryDropdownOpen] = React.useState(false); - const [localSelectedOption, setLocalSelectedOption] = React.useState( - options.length === 1 ? 0 : undefined, - ); - React.useEffect(() => { - if (options.length === 1) { - // eslint-disable-next-line react-hooks/set-state-in-effect - setLocalSelectedOption(0); - } - }, [options.length]); - const optionSelected = localSelectedOption !== undefined; - - React.useEffect(() => { - if (confirmRetries === null) { - return; - } - const timer = setTimeout(() => setConfirmRetries(null), CONFIRM_RESET_MS); - return () => clearTimeout(timer); - }, [confirmRetries]); - - const [execAgent, setExecAgent] = React.useState(proposal.spec.execution?.agent ?? ''); - React.useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect - setExecAgent(proposal.spec.execution?.agent ?? ''); - }, [proposal.spec.execution?.agent]); - - const sandboxPod = analysis?.sandbox?.claimName; - const sandboxNs = analysis?.sandbox?.namespace || 'openshift-lightspeed'; - const isAnalyzing = phase === 'Analyzing' || phase === 'Pending'; - const generation = proposal.metadata?.generation ?? 0; - const analyzedCondition = proposal.status?.conditions?.find( - (c: ProposalCondition) => c.type === 'Analyzed', - ); - const revisionPending = - !!proposal.spec.revisionFeedback && generation > (analyzedCondition?.observedGeneration ?? 0); - - const [logsExpanded, setLogsExpanded] = useAutoCollapseLogs(hasAnalysis && !revisionPending); - - const isAdvisory = !proposal.spec.execution; + ); - const [refineOpen, setRefineOpen] = React.useState(false); - const [refineFeedback, setRefineFeedback] = React.useState(''); - const [refineInProgress, setRefineInProgress] = React.useState(false); - const [refineError, setRefineError] = React.useState(null); + case 'Proposed': + return v.options.length > 0 ? ( + <> + {v.advisory && ( + + )} + {v.options.map((option) => ( + selectOption(option.index)} + onToggleExpand={() => selectOption(option.index)} + onExecute={v.advisory ? undefined : openExecuteModal} + mutationInProgress={mutationInProgress} + /> + ))} + + ) : ( + + + {t('No remediation options were generated by the analysis.')} + + + ); - const submitRefine = React.useCallback(async () => { - if (!refineFeedback.trim()) return; - setRefineInProgress(true); - setRefineError(null); - try { - await k8sPatch({ - data: [ - { - op: proposal.spec.revisionFeedback === undefined ? 'add' : 'replace', - path: '/spec/revisionFeedback', - value: refineFeedback.trim(), - }, - ], - model: LightspeedProposalModel, - resource: proposal, - }); - setRefineOpen(false); - setRefineFeedback(''); - } catch (err) { - setRefineError(err instanceof Error ? err.message : 'Failed to submit revision'); - } finally { - setRefineInProgress(false); - } - }, [proposal, refineFeedback]); + case 'Executing': + return ( + <> + {renderOptionCards({ showSpinner: true })} + {v.executionSandbox && ( + + )} + + ); - if (revisionPending) { - return ( - - - - {t('Re-analyzing with feedback...')} - - {proposal.spec.revisionFeedback && ( - - {proposal.spec.revisionFeedback} - - )} - {sandboxPod && } - - - - - ); - } + case 'Verifying': + return ( + <> + {renderOptionCards({})} + {v.execution && } + {v.verificationSandbox && ( + + )} + + ); - if (!hasAnalysis) { - if (analysisApproval.needsApproval && !sandboxPod) { - return ( - - ); + case 'Escalating': + default: + if (v.phase === 'Escalating' || TERMINAL_PHASES.includes(v.phase)) { + return ( + <> + {v.options.length > 0 && renderOptionCards({})} + {v.execution && } + {v.verification && } + + ); + } + return null; } + }; - if (isAnalyzing && sandboxPod) { - return ( - - - - {t('Analyzing...')} - - - - - - - ); + const renderOptionCards = (opts: { showSpinner?: boolean }) => { + if (!view) return null; + if (view.executedOptionIndex !== undefined && view.executedOptionIndex < view.options.length) { + const executedOption = view.options[view.executedOptionIndex]; + if (executedOption) { + return ( + selectOption(executedOption.index)} + onToggleExpand={() => selectOption(executedOption.index)} + readOnly + showSpinner={opts.showSpinner} + /> + ); + } } - const isTerminal = phase === 'Escalated' || phase === 'Failed' || phase === 'Denied'; - const message = isTerminal - ? t('Analysis did not complete — no proposal was generated.') - : t('No proposal generated yet.'); - return ( - - {message} - - ); - } - - const busy = executionApproval.inProgress || refineInProgress; + return view.options.map((option) => ( + selectOption(option.index)} + onToggleExpand={() => selectOption(option.index)} + readOnly + showSpinner={opts.showSpinner && selectedOption === option.index} + /> + )); + }; - const actionButtons = - showExecutionApproval && optionSelected ? ( - - {isAdvisory && ( - - - {t( - 'Review the advisory below and apply changes externally (e.g., via GitOps). No direct cluster execution will occur.', - )} - - - )} - - - {confirmRetries === null ? ( - <> - {agentNames.length > 0 && ( + return ( + + + {t('{{name}} details', { name: proposal?.metadata?.name || t('Proposal') })} + + + + + + { + e.preventDefault(); + navigate('/lightspeed/proposals'); + }} + > + {t('AI Hub')} + + {proposal?.metadata?.name ?? name} + + + + + + - + - )} - - - { - setRetryDropdownOpen(false); - setConfirmRetries(value as number); - }} - toggle={(toggleRef) => ( - setRetryDropdownOpen((o) => !o)} - ref={toggleRef} - splitButtonItems={[ - setConfirmRetries(0)}> - {t('Approve')} - , - ]} - variant="primary" - /> + + {proposal?.metadata?.name} + + + {view && ( + + + + + + {view.source && ( + + + )} - > - - {RETRY_OPTIONS.map((n) => ( - - {t('Approve with {{num}} retries', { num: n })} - - ))} - - - - - - - - - - - - - - ) : ( - <> - - - - - + {view.targetNamespaces?.map((ns) => ( + + + + ))} + + + )} - + + {t('Created')}{' '} + + - - )} - - - {refineOpen && ( - -