diff --git a/locales/en/plugin__lightspeed-agentic-console-plugin.json b/locales/en/plugin__lightspeed-agentic-console-plugin.json index 1d65151..54da388 100644 --- a/locales/en/plugin__lightspeed-agentic-console-plugin.json +++ b/locales/en/plugin__lightspeed-agentic-console-plugin.json @@ -86,6 +86,7 @@ "Failed to approve execution.": "Failed to approve execution.", "Failed to copy to clipboard": "Failed to copy to clipboard", "Failed to deny {{stage}}.": "Failed to deny {{stage}}.", + "Failed to load agentic capabilities configuration": "Failed to load agentic capabilities configuration", "Failed to load logs.": "Failed to load logs.", "Failed to update agentic capabilities": "Failed to update agentic capabilities", "Failure reason": "Failure reason", diff --git a/src/components/runs/AgenticCapabilitiesToggle.test.ts b/src/components/runs/AgenticCapabilitiesToggle.test.ts index f500969..697a134 100644 --- a/src/components/runs/AgenticCapabilitiesToggle.test.ts +++ b/src/components/runs/AgenticCapabilitiesToggle.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from 'vitest'; -import { buildSuspendedPatch } from './agenticCapabilitiesUtils'; +import { + buildAgenticOLSConfig, + buildSuspendedPatch, + isNotFoundError, +} from './agenticCapabilitiesUtils'; describe('buildSuspendedPatch', () => { test('returns replace op setting suspended to true', () => { @@ -14,3 +18,34 @@ describe('buildSuspendedPatch', () => { ]); }); }); + +describe('buildAgenticOLSConfig', () => { + test('builds a cluster config with the given suspended value', () => { + expect(buildAgenticOLSConfig(true)).toEqual({ + apiVersion: 'agentic.openshift.io/v1alpha1', + kind: 'AgenticOLSConfig', + metadata: { name: 'cluster' }, + spec: { suspended: true }, + }); + }); +}); + +describe('isNotFoundError', () => { + test('is true for a 404 error', () => { + expect(isNotFoundError({ code: 404 })).toBe(true); + }); + + test('is false for other error codes', () => { + expect(isNotFoundError({ code: 403 })).toBe(false); + expect(isNotFoundError({ code: 500 })).toBe(false); + }); + + test('is false for errors without a code', () => { + expect(isNotFoundError(new Error('network down'))).toBe(false); + }); + + test('is false when there is no error', () => { + expect(isNotFoundError(null)).toBe(false); + expect(isNotFoundError(undefined)).toBe(false); + }); +}); diff --git a/src/components/runs/AgenticCapabilitiesToggle.tsx b/src/components/runs/AgenticCapabilitiesToggle.tsx index 6de55c0..90367f2 100644 --- a/src/components/runs/AgenticCapabilitiesToggle.tsx +++ b/src/components/runs/AgenticCapabilitiesToggle.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { + k8sCreate, k8sPatch, useAccessReview, useK8sWatchResource, @@ -24,7 +25,12 @@ import { AgenticOLSConfigGVK, AgenticOLSConfigModel, } from '../../models/agenticrun'; -import { buildSuspendedPatch } from './agenticCapabilitiesUtils'; +import { + AGENTIC_OLS_CONFIG_NAME, + buildAgenticOLSConfig, + buildSuspendedPatch, + isNotFoundError, +} from './agenticCapabilitiesUtils'; import './AgenticCapabilitiesToggle.css'; @@ -34,9 +40,9 @@ const AgenticCapabilitiesToggle: React.FC = () => { const [error, setError] = React.useState(''); const [confirmOpen, setConfirmOpen] = React.useState(false); - const [config, loaded] = useK8sWatchResource({ + const [config, loaded, loadError] = useK8sWatchResource({ groupVersionKind: AgenticOLSConfigGVK, - name: 'cluster', + name: AGENTIC_OLS_CONFIG_NAME, }); const [canPatch] = useAccessReview({ @@ -45,6 +51,18 @@ const AgenticCapabilitiesToggle: React.FC = () => { verb: 'patch', }); + const [canCreate] = useAccessReview({ + group: AgenticOLSConfigModel.apiGroup, + resource: AgenticOLSConfigModel.plural, + verb: 'create', + }); + + const configExists = !!config?.metadata?.name; + const configAbsent = isNotFoundError(loadError); + const unknownLoadError = !!loadError && !configAbsent; + const ready = loaded || configAbsent; + const canModify = configExists ? canPatch : canCreate; + const isEnabled = !config?.spec?.suspended; const setSuspended = React.useCallback( @@ -52,11 +70,16 @@ const AgenticCapabilitiesToggle: React.FC = () => { setSaving(true); setError(''); try { - await k8sPatch({ - model: AgenticOLSConfigModel, - resource: config, - data: buildSuspendedPatch(suspended), - }); + await (configExists + ? k8sPatch({ + model: AgenticOLSConfigModel, + resource: config, + data: buildSuspendedPatch(suspended), + }) + : k8sCreate({ + model: AgenticOLSConfigModel, + data: buildAgenticOLSConfig(suspended), + })); return true; } catch (err) { setError(err instanceof Error ? err.message : String(err)); @@ -65,7 +88,7 @@ const AgenticCapabilitiesToggle: React.FC = () => { setSaving(false); } }, - [config], + [config, configExists], ); const handleToggle = React.useCallback( @@ -88,6 +111,15 @@ const AgenticCapabilitiesToggle: React.FC = () => { return ( <> + {unknownLoadError && ( + + {loadError instanceof Error ? loadError.message : String(loadError)} + + )} {error && !confirmOpen && ( setError('')} />} @@ -135,7 +167,7 @@ const AgenticCapabilitiesToggle: React.FC = () => { aria-label={t('Agentic capabilities')} id="agentic-capabilities-toggle" isChecked={isEnabled} - isDisabled={!loaded || saving || !canPatch} + isDisabled={!ready || saving || !canModify} onChange={handleToggle} /> diff --git a/src/components/runs/agenticCapabilitiesUtils.ts b/src/components/runs/agenticCapabilitiesUtils.ts index c7e2634..80e15bc 100644 --- a/src/components/runs/agenticCapabilitiesUtils.ts +++ b/src/components/runs/agenticCapabilitiesUtils.ts @@ -1,3 +1,17 @@ +import { AgenticOLSConfig, AgenticOLSConfigModel } from '../../models/agenticrun'; + +export const AGENTIC_OLS_CONFIG_NAME = 'cluster'; + +export const isNotFoundError = (loadError: unknown): boolean => + (loadError as { code?: number } | null)?.code === 404; + export const buildSuspendedPatch = (suspended: boolean) => [ { op: 'add' as const, path: '/spec/suspended', value: suspended }, ]; + +export const buildAgenticOLSConfig = (suspended: boolean): AgenticOLSConfig => ({ + apiVersion: `${AgenticOLSConfigModel.apiGroup}/${AgenticOLSConfigModel.apiVersion}`, + kind: AgenticOLSConfigModel.kind, + metadata: { name: AGENTIC_OLS_CONFIG_NAME }, + spec: { suspended }, +});