diff --git a/.ai/spec/how/k8s-data-layer.md b/.ai/spec/how/k8s-data-layer.md index 2f41f2e..71761ab 100644 --- a/.ai/spec/how/k8s-data-layer.md +++ b/.ai/spec/how/k8s-data-layer.md @@ -6,6 +6,9 @@ |---|---|---| | `src/models/proposal.ts` | `LightspeedProposalModel`, `LightspeedProposalGVK`, all `*Model`/`*GVK` constants | K8sModel definitions for the Console SDK's watch/patch/create/delete functions | | `src/models/proposal.ts` | `LightspeedProposal`, `LightspeedProposalApproval`, `*ResultCR` types | TypeScript types for each CRD | +| `src/models/proposal.ts` | `ProposalK8s`, `AnalysisResultK8s`, `ExecutionResultK8s`, `VerificationResultK8s`, `ProposalApprovalK8s` | K8s intersection types (`CRDType & K8sResourceCommon`) for `useK8sWatchResource` generics | +| `src/models/proposal-views.ts` | `ProposalView`, `RemediationOptionView`, `ExecutionView`, `VerificationView` | View-model types — output of the API→view mapping layer | +| `src/hooks/useProposal.ts` | `useProposal`, `mapToProposalView` | Fetches all proposal-related CRs and maps to a single `ProposalView` | | `src/hooks/useStageApproval.ts` | `useStageApproval` | Encapsulates approval read state + patch write in a single hook | | `src/utils/approval.ts` | `buildApprovalPatch` | Generates JSON Patch arrays for `AgenticRunApproval` mutations | | `src/config.ts` | `getApiUrl` | Constructs backend proxy URLs | @@ -33,6 +36,8 @@ useK8sWatchResource(AnalysisResultGVK, {namespace, selector: {matchLabels: {agen This pattern repeats for ExecutionResult, VerificationResult, and EscalationResult. The `results[]` array on each step status contains `{name, outcome}` refs — the name matches the result CR's `metadata.name`. +The `useProposal` hook wraps all five watches (Proposal, AnalysisResult, ExecutionResult, VerificationResult, ProposalApproval) and uses `filterLatest` to select the most recent result CR by `creationTimestamp`. The mapped `ProposalView` is recomputed via `useMemo` whenever any watched resource changes. + ### Approval Patch Generation `buildApprovalPatch` handles three structural cases: @@ -60,6 +65,8 @@ Every CRD has a paired `K8sModel` (used by Console SDK functions) and a `GVK` ob CRD types are hand-written, not generated. A TODO exists to auto-generate from OpenAPI. The types closely mirror the CRD status structure — changes in the operator's CRD require manual synchronization here. +K8s intersection types (e.g., `ProposalK8s = LightspeedProposal & K8sResourceCommon`) are defined at the bottom of `proposal.ts` for use with `useK8sWatchResource` generics. A separate view-model layer in `proposal-views.ts` defines UI-optimized types (`ProposalView`, `RemediationOptionView`, etc.) with `*View` suffix. The `useProposal` hook in `src/hooks/useProposal.ts` contains pure mapping functions (`mapRootCause`, `mapOption`, `mapExecution`, `mapVerification`, `mapTimeline`) that transform API types into view types. Phase derivation is centralized in `derivePhaseFromConditions` (defined in `proposal.ts`, used by both list and detail pages). + ### Approval State Machine The `useStageApproval` hook combines: diff --git a/.ai/spec/how/project-structure.md b/.ai/spec/how/project-structure.md index 44fad8e..1ef1ccc 100644 --- a/.ai/spec/how/project-structure.md +++ b/.ai/spec/how/project-structure.md @@ -9,13 +9,22 @@ | `src/utils/approval.ts` | `findStage`, `getStageStatus`, `stageNeedsApproval`, `buildApprovalPatch` | Pure functions for approval logic | | `src/hooks/useStageApproval.ts` | `useStageApproval` | React hook wrapping approval state and K8s patch operations | | `src/utils/markdown.ts` | — | Markdown rendering utilities | -| `src/components/proposals/ProposalListPage.tsx` | `ProposalListPage` | Run list with virtualized table and phase filters | -| `src/components/proposals/ProposalDetailPage.tsx` | `ProposalDetailPage`, `OverviewTab`, `ProposalTab`, `ResultTab`, `VerificationTab`, `EscalationTab` | Multi-tab run detail with approval flows | -| `src/components/proposals/SandboxLogViewer.tsx` | `SandboxLogViewer` | Real-time pod log streaming with reconnection | -| `src/components/proposals/EscalateModal.tsx` | `EscalateModal` | Escalation confirmation modal | -| `src/components/proposals/MarkdownText.tsx` | `MarkdownText` | Sanitized markdown-to-HTML rendering | -| `src/components/proposals/PhaseIcon.tsx` | `PhaseIcon` | Phase status icon with failure indicators | -| `src/components/proposals/DynamicComponent.tsx` | `DynamicComponent` | Component registry dispatch for adapter components | +| `src/components/proposals/ProposalListPage.tsx` | `ProposalListPage` | Proposal list with virtualized table and phase filters | +| `src/components/proposals/ProposalDetailPage.tsx` | `ProposalDetailsPage` | Section-based proposal detail page, delegates to `detail/` subcomponents | +| `src/components/proposals/detail/AnalysisSummary.tsx` | `AnalysisSummary` | Root cause display, analysis loading/streaming state | +| `src/components/proposals/detail/RemediationOptionCard.tsx` | `RemediationOptionCard` | Expandable remediation option card | +| `src/components/proposals/detail/ExecutionSummary.tsx` | `ExecutionSummary` | Post-execution actions and outcome display | +| `src/components/proposals/detail/VerificationSummary.tsx` | `VerificationSummary` | Verification checks and summary | +| `src/components/proposals/detail/ProposalPhaseLabel.tsx` | `ProposalPhaseLabel` | Phase label with status color | +| `src/components/proposals/detail/ProposalTimeline.tsx` | `ProposalTimeline` | Chronological event timeline | +| `src/components/proposals/detail/StageInProgress.tsx` | `StageInProgress` | In-progress stage card with embedded log viewer | +| `src/components/proposals/detail/SandboxLogViewer.tsx` | `SandboxLogViewer` | Expandable log viewer with streaming and search | +| `src/components/StatusGuard.tsx` | `StatusGuard` | Loading/error/empty gate using PatternFly `ErrorState`; replaces internal console `StatusBox` | +| `src/models/proposal-views.ts` | `ProposalView`, `RemediationOptionView`, `ExecutionView`, `VerificationView`, `SandboxView`, `TimelineEvent`, `TERMINAL_PHASES` | View-model types for the detail page (output of `useProposal` mapping layer) | +| `src/constants.ts` | `PROPOSAL_NAMESPACE`, `PROPOSAL_LABEL_SOURCE`, `RESULT_LABEL_PROPOSAL` | Shared constants for K8s label keys and namespace | +| `src/hooks/useProposal.ts` | `useProposal`, `mapRootCause`, `mapOption`, `mapExecution`, `mapVerification`, `mapTimeline`, `filterLatest` | Fetches proposal + result CRs, maps API types → view types | +| `src/hooks/useExecutionLogActions.ts` | `useExecutionLogActions` | Parses execution actions from sandbox pod logs | +| `src/hooks/useSandboxLogStream.ts` | `useSandboxLogStream` | Streams audit lines from sandbox pod logs | | `src/components/proposals/dynamic/` | `ResourceDiff`, `Visualization`, `DataTable`, `ActionPicker`, `EvidenceTable`, `StatusTimeline`, `CmoComponents` | Individual dynamic component renderers | | `src/components/configuration/ConfigurationPage.tsx` | `ConfigurationPage` | Configuration page with tabbed layout | | `src/components/configuration/ApprovalPolicyTab.tsx` | `ApprovalPolicyTab` | Approval policy CRUD | @@ -42,6 +51,7 @@ The plugin has no traditional `main` entry point. Webpack's `ConsoleRemotePlugin - Components: PascalCase `.tsx` files, one primary component per file, default export. - CSS: co-located `.css` files alongside components. All classes prefixed `ols-plugin__`. -- Models: singular `proposal.ts` contains all CRD types (not split by CRD). +- Models: `proposal.ts` contains all CRD types and K8s intersection types. `proposal-views.ts` contains view-model types (`*View` suffix) for the detail page. - Hooks: `use` prefix, one hook per file in `src/hooks/`. +- Detail subcomponents: each section of the detail page is a separate component in `src/components/proposals/detail/`, imported by `ProposalDetailPage.tsx`. - Dynamic components: each renderer in `src/components/proposals/dynamic/`, re-exported via `index.tsx`. diff --git a/.ai/spec/what/run-lifecycle.md b/.ai/spec/what/run-lifecycle.md index a418f96..b3fe5ae 100644 --- a/.ai/spec/what/run-lifecycle.md +++ b/.ai/spec/what/run-lifecycle.md @@ -22,13 +22,12 @@ The core domain of the plugin: displaying and managing runs through a multi-stag 8. The list MUST support filtering by phase and text search. 9. Each row MUST show: name (linked to detail), phase label, request preview (truncated to 80 chars), namespace, and age. -### Run Detail — Tabs +### Run Detail — Layout -10. The detail page MUST show tabs: Overview, Proposal, Execution, Verification, and conditionally Escalation. -11. The Escalation tab MUST only appear when the run has an `Escalated` condition. -12. For CMO (cluster-monitoring-operator) sourced runs, the Overview tab is hidden and the default tab is Proposal. -13. A blue dot indicator MUST appear on the tab corresponding to the currently active phase when the user is viewing a different tab. -14. Tabs that need approval MUST show a "Needs approval" badge. +10. The detail page MUST gate its content behind a loading/error guard (`StatusGuard`): show a spinner while loading, an error state on failure (403 → restricted access, 404 → not found, other → error message with detail), and the page content when data is ready. +11. The detail page uses a single-page section layout (not tabs). Sections are rendered conditionally based on the current phase: Analysis summary, Remediation options, Execution summary, Verification summary, and Timeline. +12. During in-progress stages (Analyzing, Executing, Verifying), the page MUST show a `StageInProgress` card with embedded live log streaming from the sandbox pod. +13. The page MUST be wrapped in `AgenticLayout` to display the system-suspended banner when the agentic config has `suspended: true`. ### Approval Flow diff --git a/locales/en/plugin__lightspeed-agentic-console-plugin.json b/locales/en/plugin__lightspeed-agentic-console-plugin.json index 8af40b8..30a87a6 100644 --- a/locales/en/plugin__lightspeed-agentic-console-plugin.json +++ b/locales/en/plugin__lightspeed-agentic-console-plugin.json @@ -1,197 +1,133 @@ { + "{{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", "Agents": "Agents", "AI Hub": "AI Hub", "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", - "Approve with retries": "Approve with 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" + "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}}." } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 761e7fa..2dd5ac3 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": { @@ -5070,6 +5216,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", @@ -5148,6 +5305,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", @@ -7031,6 +7198,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, @@ -7815,6 +7983,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.1", "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz", @@ -8335,6 +8509,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", @@ -8845,6 +9025,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", @@ -9128,6 +9474,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", @@ -10454,6 +10806,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", @@ -10523,6 +10881,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", @@ -10701,6 +11081,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", @@ -11316,6 +11703,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", @@ -12263,6 +12656,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", @@ -12522,6 +12924,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", @@ -12563,6 +12983,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 a2f18b8..3745667 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/__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/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/configuration/AgentForm.tsx b/src/components/configuration/AgentForm.tsx index 6e75d4b..f4aa2ef 100644 --- a/src/components/configuration/AgentForm.tsx +++ b/src/components/configuration/AgentForm.tsx @@ -28,6 +28,7 @@ const AgentForm: React.FC = ({ providers, onSubmit, onCancel }) React.useEffect(() => { if (!providerName && providers.length > 0) { + // eslint-disable-next-line react-hooks/set-state-in-effect setProviderName(providers[0].metadata.name); } }, [providers, providerName]); diff --git a/src/components/configuration/ApprovalPolicyTab.tsx b/src/components/configuration/ApprovalPolicyTab.tsx index a4fae4e..7cd0ec1 100644 --- a/src/components/configuration/ApprovalPolicyTab.tsx +++ b/src/components/configuration/ApprovalPolicyTab.tsx @@ -52,16 +52,18 @@ const ApprovalPolicyTab: React.FC = () => { const policyResourceVersion = policy?.metadata?.resourceVersion; React.useEffect(() => { - if (policyExists) { - const stages = policy.spec?.stages; - setStageValues({ - Analysis: getStageApproval(stages, 'Analysis'), - Execution: getStageApproval(stages, 'Execution'), - Verification: getStageApproval(stages, 'Verification'), - Escalation: getStageApproval(stages, 'Escalation'), - }); - setMaxAttempts(policy.spec?.maxAttempts ?? 1); - } + if (!policyExists) return; + const stages = policy.spec?.stages; + // eslint-disable-next-line react-hooks/set-state-in-effect + setStageValues({ + Analysis: getStageApproval(stages, 'Analysis'), + Execution: getStageApproval(stages, 'Execution'), + Verification: getStageApproval(stages, 'Verification'), + Escalation: getStageApproval(stages, 'Escalation'), + }); + + setMaxAttempts(policy.spec?.maxAttempts ?? 1); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [policyResourceVersion]); const handleSave = async () => { 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 cc7fd1f..b1d8500 100644 --- a/src/components/proposals/ProposalDetailPage.tsx +++ b/src/components/proposals/ProposalDetailPage.tsx @@ -1,2143 +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 confidenceColor = (c: string): 'green' | 'orange' | 'red' | 'grey' => { - switch (c) { - case 'high': - return 'green'; - case 'medium': - return 'orange'; - case 'low': - return 'red'; - default: - return 'grey'; - } -}; - -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 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 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); - - React.useEffect(() => { - if (prevRef.current === serialized) { - return; - } - prevRef.current = serialized; - setHighlight(true); - const timer = setTimeout(() => setHighlight(false), 1500); - return () => clearTimeout(timer); - }, [serialized]); - - return ( - - {diagnosis && ( - + 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); + + setExpandedOption(idx); + }, [phaseKey, executedOptionIndex]); + + const selectOption = useCallback((idx: number) => { + setSelectedOption(idx); + setExpandedOption((prev) => (prev === idx ? -1 : idx)); + }, []); + + const [executeOptionIndex, setExecuteOptionIndex] = useState(null); + + const openExecuteModal = useCallback(() => { + setExecuteOptionIndex(selectedOption); + }, [selectedOption]); + + const handleApproveExecution = useCallback(async () => { + if (executeOptionIndex === null) return; + const success = await approveExecution(executeOptionIndex); + if (success) setExecuteOptionIndex(null); + }, [approveExecution, executeOptionIndex]); + + 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) { - 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(() => { - 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 && ( - -