Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions locales/en/plugin__cluster-update-console-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
"Error loading cluster version": "Error loading cluster version",
"Generated": "Generated",
"Lifecycle": "Lifecycle",
"Lightspeed proposals unavailable": "Lightspeed proposals unavailable",
"Lightspeed agentic runs unavailable": "Lightspeed agentic runs unavailable",
"Loading": "Loading",
"Minor": "Minor",
"Name": "Name",
"No": "No",
"No active update plans": "No active update plans",
"No agentic runs available.": "No agentic runs available.",
"No update history available": "No update history available",
"No update proposals available.": "No update proposals available.",
"OLM operators": "OLM operators",
"OLM Operators": "OLM Operators",
"Operator": "Operator",
Expand All @@ -48,14 +48,14 @@
"Review available versions, assess operator compatibility, and plan how this cluster version is newer OpenShift releases. Use Updates plan to prepare or start an update, Active update plans for in-flight work, and Update history for completed ones.": "Review available versions, assess operator compatibility, and plan how this cluster version is newer OpenShift releases. Use Updates plan to prepare or start an update, Active update plans for in-flight work, and Update history for completed ones.",
"Sandbox: {{name}}": "Sandbox: {{name}}",
"Schedule for later": "Schedule for later",
"Select proposal": "Select proposal",
"Select agentic run": "Select agentic run",
"Select Update Path": "Select Update Path",
"Started": "Started",
"Starting analysis — waiting for agent sandbox...": "Starting analysis — waiting for agent sandbox...",
"Status": "Status",
"Target Version": "Target Version",
"Target version {{version}} not found in available updates": "Target version {{version}} not found in available updates",
"The Lightspeed Proposal CRD is not installed on this cluster. AI-driven update planning features are disabled.": "The Lightspeed Proposal CRD is not installed on this cluster. AI-driven update planning features are disabled.",
"The Lightspeed AgenticRun CRD is not installed on this cluster. AI-driven update planning features are disabled.": "The Lightspeed AgenticRun CRD is not installed on this cluster. AI-driven update planning features are disabled.",
"There are no update plans currently in progress.": "There are no update plans currently in progress.",
"This cluster has no recorded update history.": "This cluster has no recorded update history.",
"unknown": "unknown",
Expand Down
40 changes: 24 additions & 16 deletions src/__tests__/proposal.test.ts → src/__tests__/agenticrun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import {
derivePhase,
COMPONENT_TYPES,
AdapterComponent,
LightspeedProposal,
LightspeedAgenticRun,
LightspeedAnalysisResult,
OtaReadinessSummary,
OtaFinding,
OtaOlmOperatorStatus,
} from '../models/proposal';
} from '../models/agenticrun';

describe('getPhaseDisplay', () => {
it('maps known phases to expected colors', () => {
Expand Down Expand Up @@ -62,7 +62,7 @@ describe('getAnalysisDataFromResult', () => {
});

it('returns empty components when result has no options', () => {
const result = { spec: { proposalName: 'test' }, status: {} } as LightspeedAnalysisResult;
const result = { spec: { agenticRunName: 'test' }, status: {} } as LightspeedAnalysisResult;
const data = getAnalysisDataFromResult(result);
expect(data.components).toEqual([]);
});
Expand All @@ -73,7 +73,7 @@ describe('getAnalysisDataFromResult', () => {
{ type: 'ota_finding', severity: 'info', check: 'test', detail: 'ok' },
];
const result = {
spec: { proposalName: 'test' },
spec: { agenticRunName: 'test' },
status: { options: [{ title: 'Option', components: { analysisData: components } }] },
} as unknown as LightspeedAnalysisResult;
const data = getAnalysisDataFromResult(result);
Expand All @@ -84,7 +84,7 @@ describe('getAnalysisDataFromResult', () => {
it('extracts legacy flat object from analysisData', () => {
const legacyData = { decision: 'recommend', summary: 'All good' };
const result = {
spec: { proposalName: 'test' },
spec: { agenticRunName: 'test' },
status: { options: [{ title: 'Option', components: { analysisData: legacyData } }] },
} as unknown as LightspeedAnalysisResult;
const data = getAnalysisDataFromResult(result);
Expand All @@ -94,35 +94,43 @@ describe('getAnalysisDataFromResult', () => {
});

describe('derivePhase', () => {
const makeProposal = (conditions: { type: string; status: string; reason?: string }[]): LightspeedProposal =>
const makeAgenticRun = (
conditions: { type: string; status: string; reason?: string }[],
): LightspeedAgenticRun =>
({
spec: { request: 'test', analysis: { agent: 'default' } },
status: { conditions },
}) as unknown as LightspeedProposal;
}) as unknown as LightspeedAgenticRun;

it('returns Pending when no conditions', () => {
expect(derivePhase(undefined)).toBe('Pending');
expect(derivePhase(makeProposal([]))).toBe('Pending');
expect(derivePhase(makeAgenticRun([]))).toBe('Pending');
});

it('returns Analyzing when Analyzed=False', () => {
expect(derivePhase(makeProposal([{ type: 'Analyzed', status: 'False' }]))).toBe('Analyzing');
expect(derivePhase(makeAgenticRun([{ type: 'Analyzed', status: 'False' }]))).toBe('Analyzing');
});

it('returns Failed when Analyzed=False with reason Failed', () => {
expect(derivePhase(makeProposal([{ type: 'Analyzed', status: 'False', reason: 'Failed' }]))).toBe('Failed');
expect(
derivePhase(makeAgenticRun([{ type: 'Analyzed', status: 'False', reason: 'Failed' }])),
).toBe('Failed');
});

it('returns Analysed when analysis-only (execution/verification skipped)', () => {
expect(derivePhase(makeProposal([
{ type: 'Analyzed', status: 'True' },
{ type: 'Executed', status: 'True', reason: 'Skipped' },
{ type: 'Verified', status: 'True', reason: 'Skipped' },
]))).toBe('Analysed');
expect(
derivePhase(
makeAgenticRun([
{ type: 'Analyzed', status: 'True' },
{ type: 'Executed', status: 'True', reason: 'Skipped' },
{ type: 'Verified', status: 'True', reason: 'Skipped' },
]),
),
).toBe('Analysed');
});

it('returns Escalated when Escalated=True', () => {
expect(derivePhase(makeProposal([{ type: 'Escalated', status: 'True' }]))).toBe('Escalated');
expect(derivePhase(makeAgenticRun([{ type: 'Escalated', status: 'True' }]))).toBe('Escalated');
});
});

Expand Down
28 changes: 14 additions & 14 deletions src/components/ClusterUpdatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {
Title,
} from '@patternfly/react-core';
import { useClusterVersion } from '../hooks/useClusterVersion';
import { useUpdateProposals } from '../hooks/useUpdateProposals';
import { useAgenticRuns } from '../hooks/useAgenticRuns';
import { I18N_NAMESPACE, LABELS, TERMINAL_PHASES } from '../utils/constants';
import { LightspeedProposal, derivePhase } from '../models/proposal';
import { LightspeedAgenticRun, derivePhase } from '../models/agenticrun';
import { ClusterVersion } from '../models/clusterversion';
import UpdatePlanTab from './update-plan/UpdatePlanTab';
import ActivePlansTab from './active-plans/ActivePlansTab';
Expand All @@ -28,20 +28,20 @@ export default function ClusterUpdatePage() {
const [activeTab, setActiveTab] = React.useState<string | number>(0);

const [clusterVersion, cvLoaded, cvError] = useClusterVersion();
const [proposalsRaw, proposalsLoaded, proposalsError] = useUpdateProposals();
const [agenticRunsRaw, agenticRunsLoaded, agenticRunsError] = useAgenticRuns();

const proposalsAvailable = proposalsLoaded && !proposalsError;
const proposals: LightspeedProposal[] = React.useMemo(
const agenticRunsAvailable = agenticRunsLoaded && !agenticRunsError;
const agenticRuns: LightspeedAgenticRun[] = React.useMemo(
() =>
(proposalsAvailable ? (proposalsRaw ?? []) : []).filter(
(p: LightspeedProposal) => p.metadata?.labels?.[LABELS.updateType] !== undefined,
(agenticRunsAvailable ? (agenticRunsRaw ?? []) : []).filter(
(p: LightspeedAgenticRun) => p.metadata?.labels?.[LABELS.updateType] !== undefined,
),
[proposalsAvailable, proposalsRaw],
[agenticRunsAvailable, agenticRunsRaw],
);

const activePlans = React.useMemo(
() => proposals.filter((p: LightspeedProposal) => !TERMINAL_PHASES.has(derivePhase(p))),
[proposals],
() => agenticRuns.filter((p: LightspeedAgenticRun) => !TERMINAL_PHASES.has(derivePhase(p))),
[agenticRuns],
);

const pageTitle = t('Cluster Update');
Expand All @@ -65,11 +65,11 @@ export default function ClusterUpdatePage() {
</Alert>
</PageSection>
)}
{proposalsError && (
{agenticRunsError && (
<PageSection hasBodyWrapper={false}>
<Alert variant="warning" isInline title={t('Lightspeed proposals unavailable')}>
<Alert variant="warning" isInline title={t('Lightspeed agentic runs unavailable')}>
{t(
'The Lightspeed Proposal CRD is not installed on this cluster. AI-driven update planning features are disabled.',
'The Lightspeed AgenticRun CRD is not installed on this cluster. AI-driven update planning features are disabled.',
)}
</Alert>
</PageSection>
Expand All @@ -91,7 +91,7 @@ export default function ClusterUpdatePage() {
{activeTab === 0 && (
<UpdatePlanTab
clusterVersion={clusterVersion as ClusterVersion}
proposals={proposals}
agenticRuns={agenticRuns}
/>
)}
</TabContentBody>
Expand Down
20 changes: 10 additions & 10 deletions src/components/active-plans/ActivePlansTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { Timestamp } from '@openshift-console/dynamic-plugin-sdk';
import { EmptyState, EmptyStateBody } from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table';
import { LightspeedProposal, derivePhase } from '../../models/proposal';
import { LightspeedAgenticRun, derivePhase } from '../../models/agenticrun';
import { I18N_NAMESPACE, LABELS } from '../../utils/constants';
import PhaseLabel from '../shared/PhaseLabel';
import './active-plans.css';

type ActivePlansTabProps = {
activePlans: LightspeedProposal[];
activePlans: LightspeedAgenticRun[];
};

const ActivePlansTab: React.FC<ActivePlansTabProps> = ({ activePlans }) => {
Expand All @@ -38,27 +38,27 @@ const ActivePlansTab: React.FC<ActivePlansTabProps> = ({ activePlans }) => {
</Tr>
</Thead>
<Tbody>
{activePlans.map((proposal) => {
const targetVersion = proposal.metadata?.labels?.[LABELS.targetVersion] ?? '-';
const updateType = proposal.metadata?.labels?.[LABELS.updateType] ?? '-';
{activePlans.map((agenticRun) => {
const targetVersion = agenticRun.metadata?.labels?.[LABELS.targetVersion] ?? '-';
const updateType = agenticRun.metadata?.labels?.[LABELS.updateType] ?? '-';

return (
<Tr key={proposal.metadata?.uid ?? proposal.metadata?.name}>
<Tr key={agenticRun.metadata?.uid ?? agenticRun.metadata?.name}>
<Td dataLabel={t('Name')}>
<Link
to={`/lightspeed/proposals/${proposal.metadata?.namespace}/${proposal.metadata?.name}`}
to={`/lightspeed/runs/${agenticRun.metadata?.namespace}/${agenticRun.metadata?.name}`}
className="cluster-update-plugin__check-link"
>
{proposal.metadata?.name}
{agenticRun.metadata?.name}
</Link>
</Td>
<Td dataLabel={t('Target Version')}>{targetVersion}</Td>
<Td dataLabel={t('Phase')}>
<PhaseLabel phase={derivePhase(proposal)} />
<PhaseLabel phase={derivePhase(agenticRun)} />
</Td>
<Td dataLabel={t('Update Type')}>{updateType}</Td>
<Td dataLabel={t('Age')}>
<Timestamp timestamp={proposal.metadata?.creationTimestamp} simple />
<Timestamp timestamp={agenticRun.metadata?.creationTimestamp} simple />
</Td>
</Tr>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/shared/PhaseIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import {
QuestionCircleIcon,
} from '@patternfly/react-icons';
import { Icon } from '@patternfly/react-core';
import { ProposalPhase } from '../../models/proposal';
import { AgenticRunPhase } from '../../models/agenticrun';

type PhaseIconProps = {
phase?: ProposalPhase | string;
phase?: AgenticRunPhase | string;
};

const PhaseIcon: React.FC<PhaseIconProps> = ({ phase }) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/shared/PhaseLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from 'react';
import { Label } from '@patternfly/react-core';
import { getPhaseDisplay, ProposalPhase } from '../../models/proposal';
import { getPhaseDisplay, AgenticRunPhase } from '../../models/agenticrun';
import PhaseIcon from './PhaseIcon';

type PhaseLabelProps = {
phase?: ProposalPhase | string;
phase?: AgenticRunPhase | string;
};

const PhaseLabel: React.FC<PhaseLabelProps> = ({ phase }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/shared/SeverityLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Label } from '@patternfly/react-core';
import { SEVERITY_COLORS, SEVERITY_LABELS } from '../../models/proposal';
import { SEVERITY_COLORS, SEVERITY_LABELS } from '../../models/agenticrun';
import { I18N_NAMESPACE } from '../../utils/constants';

type SeverityLabelProps = {
Expand Down
Loading