Skip to content

Commit fbd5e02

Browse files
authored
Merge pull request #3 from harche/wt/e2e-testing
Add Configuration page for Approval Policy, LLM Providers, and Agents
2 parents 28fca27 + 5e5e0bc commit fbd5e02

11 files changed

Lines changed: 1165 additions & 7 deletions

console-extensions.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
"component": { "$codeRef": "ProposalDetailPage" }
1515
}
1616
},
17+
{
18+
"type": "console.page/route",
19+
"properties": {
20+
"exact": true,
21+
"path": "/lightspeed/configuration",
22+
"component": { "$codeRef": "ConfigurationPage" }
23+
}
24+
},
1725
{
1826
"type": "console.navigation/href",
1927
"properties": {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@
9292
"description": "AI-driven proposals UI for OpenShift cluster operations",
9393
"exposedModules": {
9494
"ProposalListPage": "./components/proposals/ProposalListPage",
95-
"ProposalDetailPage": "./components/proposals/ProposalDetailPage"
95+
"ProposalDetailPage": "./components/proposals/ProposalDetailPage",
96+
"ConfigurationPage": "./components/configuration/ConfigurationPage"
9697
},
9798
"dependencies": {
9899
"@console/pluginAPI": "^4.21.0"
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import * as React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import {
4+
Button,
5+
ExpandableSection,
6+
FormGroup,
7+
FormSelect,
8+
FormSelectOption,
9+
NumberInput,
10+
TextInput,
11+
Title,
12+
} from '@patternfly/react-core';
13+
14+
import { LLMProviderK8s } from '../../models/proposal';
15+
16+
type AgentFormProps = {
17+
providers: LLMProviderK8s[];
18+
onSubmit: (name: string, spec: Record<string, unknown>) => Promise<void>;
19+
onCancel: () => void;
20+
};
21+
22+
const AgentForm: React.FC<AgentFormProps> = ({ providers, onSubmit, onCancel }) => {
23+
const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin');
24+
25+
const [name, setName] = React.useState('');
26+
const [providerName, setProviderName] = React.useState('');
27+
const [model, setModel] = React.useState('');
28+
29+
React.useEffect(() => {
30+
if (!providerName && providers.length > 0) {
31+
setProviderName(providers[0].metadata.name);
32+
}
33+
}, [providers, providerName]);
34+
const [maxTurns, setMaxTurns] = React.useState(100);
35+
const [analysisSeconds, setAnalysisSeconds] = React.useState(300);
36+
const [executionSeconds, setExecutionSeconds] = React.useState(600);
37+
const [verificationSeconds, setVerificationSeconds] = React.useState(300);
38+
const [chatSeconds, setChatSeconds] = React.useState(120);
39+
const [showTimeouts, setShowTimeouts] = React.useState(false);
40+
const [submitting, setSubmitting] = React.useState(false);
41+
const [error, setError] = React.useState('');
42+
43+
const buildSpec = (): Record<string, unknown> => {
44+
const spec: Record<string, unknown> = {
45+
llmProvider: { name: providerName },
46+
model,
47+
maxTurns,
48+
};
49+
if (showTimeouts) {
50+
spec.timeouts = {
51+
analysisSeconds,
52+
executionSeconds,
53+
verificationSeconds,
54+
chatSeconds,
55+
};
56+
}
57+
return spec;
58+
};
59+
60+
const handleSubmit = async () => {
61+
setSubmitting(true);
62+
setError('');
63+
try {
64+
await onSubmit(name, buildSpec());
65+
} catch (err) {
66+
setError(err instanceof Error ? err.message : String(err));
67+
setSubmitting(false);
68+
}
69+
};
70+
71+
const isValid = (): boolean => {
72+
return !!name && !!providerName && !!model;
73+
};
74+
75+
const clampedNumberInput = (
76+
value: number,
77+
min: number,
78+
max: number,
79+
setter: (v: number) => void,
80+
) => (
81+
<NumberInput
82+
value={value}
83+
min={min}
84+
max={max}
85+
onMinus={() => setter(Math.max(min, value - 30))}
86+
onPlus={() => setter(Math.min(max, value + 30))}
87+
onChange={(e) => {
88+
const val = Number((e.target as HTMLInputElement).value);
89+
if (val >= min && val <= max) setter(val);
90+
}}
91+
/>
92+
);
93+
94+
return (
95+
<div className="ols-plugin__config-form-section">
96+
<Title headingLevel="h3">{t('Create Agent')}</Title>
97+
98+
{error && <p className="ols-plugin__config-error-text">{error}</p>}
99+
100+
<FormGroup label={t('Name')} isRequired fieldId="agent-name">
101+
<TextInput id="agent-name" value={name} onChange={(_e, v) => setName(v)} isRequired placeholder="default" />
102+
</FormGroup>
103+
104+
<FormGroup label={t('LLM Provider')} isRequired fieldId="agent-provider">
105+
<FormSelect
106+
id="agent-provider"
107+
value={providerName}
108+
onChange={(_e, v) => setProviderName(v)}
109+
>
110+
{providers.length ? (
111+
providers.map((p) => (
112+
<FormSelectOption key={p.metadata.name} value={p.metadata.name} label={p.metadata.name} />
113+
))
114+
) : (
115+
<FormSelectOption value="" label={t('No providers available')} isDisabled />
116+
)}
117+
</FormSelect>
118+
</FormGroup>
119+
120+
<FormGroup label={t('Model')} isRequired fieldId="agent-model">
121+
<TextInput
122+
id="agent-model"
123+
value={model}
124+
onChange={(_e, v) => setModel(v)}
125+
isRequired
126+
placeholder="claude-opus-4-6"
127+
/>
128+
</FormGroup>
129+
130+
<FormGroup label={t('Max Turns')} fieldId="agent-max-turns">
131+
{clampedNumberInput(maxTurns, 1, 500, setMaxTurns)}
132+
</FormGroup>
133+
134+
<ExpandableSection
135+
toggleText={showTimeouts ? t('Hide Timeouts') : t('Show Timeouts')}
136+
isExpanded={showTimeouts}
137+
onToggle={(_e, expanded) => setShowTimeouts(expanded)}
138+
>
139+
<FormGroup label={t('Analysis (seconds)')} fieldId="agent-timeout-analysis">
140+
{clampedNumberInput(analysisSeconds, 1, 3600, setAnalysisSeconds)}
141+
</FormGroup>
142+
<FormGroup label={t('Execution (seconds)')} fieldId="agent-timeout-execution">
143+
{clampedNumberInput(executionSeconds, 1, 3600, setExecutionSeconds)}
144+
</FormGroup>
145+
<FormGroup label={t('Verification (seconds)')} fieldId="agent-timeout-verification">
146+
{clampedNumberInput(verificationSeconds, 1, 3600, setVerificationSeconds)}
147+
</FormGroup>
148+
<FormGroup label={t('Chat (seconds)')} fieldId="agent-timeout-chat">
149+
{clampedNumberInput(chatSeconds, 1, 600, setChatSeconds)}
150+
</FormGroup>
151+
</ExpandableSection>
152+
153+
<div className="ols-plugin__config-form-actions">
154+
<Button variant="primary" onClick={handleSubmit} isLoading={submitting} isDisabled={!isValid() || submitting}>
155+
{t('Create')}
156+
</Button>
157+
<Button variant="link" onClick={onCancel} isDisabled={submitting}>
158+
{t('Cancel')}
159+
</Button>
160+
</div>
161+
</div>
162+
);
163+
};
164+
165+
export default AgentForm;
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import * as React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import {
4+
k8sCreate,
5+
k8sDelete,
6+
Timestamp,
7+
useK8sWatchResource,
8+
} from '@openshift-console/dynamic-plugin-sdk';
9+
import {
10+
Alert,
11+
Button,
12+
Dropdown,
13+
DropdownItem,
14+
DropdownList,
15+
MenuToggle,
16+
Spinner,
17+
} from '@patternfly/react-core';
18+
import { EllipsisVIcon } from '@patternfly/react-icons';
19+
import {
20+
Table,
21+
Tbody,
22+
Td,
23+
Th,
24+
Thead,
25+
Tr,
26+
} from '@patternfly/react-table';
27+
28+
import {
29+
AgentK8s,
30+
LLMProviderK8s,
31+
LightspeedAgentGVK,
32+
LightspeedAgentModel,
33+
LightspeedLLMProviderGVK,
34+
} from '../../models/proposal';
35+
import AgentForm from './AgentForm';
36+
37+
const AgentsTab: React.FC = () => {
38+
const { t } = useTranslation('plugin__lightspeed-agentic-console-plugin');
39+
40+
const [agents, agentsLoaded, agentsError] = useK8sWatchResource<AgentK8s[]>({
41+
groupVersionKind: LightspeedAgentGVK,
42+
isList: true,
43+
});
44+
45+
const [providers, providersLoaded] = useK8sWatchResource<LLMProviderK8s[]>({
46+
groupVersionKind: LightspeedLLMProviderGVK,
47+
isList: true,
48+
});
49+
50+
const [showForm, setShowForm] = React.useState(false);
51+
const [error, setError] = React.useState('');
52+
const [openKebab, setOpenKebab] = React.useState<string | null>(null);
53+
54+
const handleCreate = async (name: string, spec: Record<string, unknown>) => {
55+
setError('');
56+
try {
57+
await k8sCreate({
58+
model: LightspeedAgentModel,
59+
data: {
60+
apiVersion: 'agentic.openshift.io/v1alpha1',
61+
kind: 'Agent',
62+
metadata: { name },
63+
spec,
64+
},
65+
});
66+
setShowForm(false);
67+
} catch (err) {
68+
setError(err instanceof Error ? err.message : String(err));
69+
}
70+
};
71+
72+
const handleDelete = async (agent: AgentK8s) => {
73+
setError('');
74+
setOpenKebab(null);
75+
try {
76+
await k8sDelete({ model: LightspeedAgentModel, resource: agent });
77+
} catch (err) {
78+
setError(err instanceof Error ? err.message : String(err));
79+
}
80+
};
81+
82+
if (!agentsLoaded || !providersLoaded) {
83+
return <Spinner size="lg" />;
84+
}
85+
86+
return (
87+
<>
88+
{(error || agentsError) && (
89+
<Alert variant="danger" isInline title={t('Error')}>
90+
{error || String(agentsError)}
91+
</Alert>
92+
)}
93+
94+
<div className="ols-plugin__config-table-actions">
95+
<Button variant="primary" onClick={() => setShowForm(true)} isDisabled={showForm}>
96+
{t('Create Agent')}
97+
</Button>
98+
</div>
99+
100+
<Table variant="compact">
101+
<Thead>
102+
<Tr>
103+
<Th>{t('Name')}</Th>
104+
<Th>{t('LLM Provider')}</Th>
105+
<Th>{t('Model')}</Th>
106+
<Th>{t('Max Turns')}</Th>
107+
<Th>{t('Age')}</Th>
108+
<Th />
109+
</Tr>
110+
</Thead>
111+
<Tbody>
112+
{agents?.length ? (
113+
agents.map((a) => (
114+
<Tr key={a.metadata.name}>
115+
<Td>{a.metadata.name}</Td>
116+
<Td>{a.spec.llmProvider?.name}</Td>
117+
<Td>{a.spec.model}</Td>
118+
<Td>{a.spec.maxTurns ?? '-'}</Td>
119+
<Td>
120+
<Timestamp timestamp={a.metadata.creationTimestamp} />
121+
</Td>
122+
<Td isActionCell>
123+
<Dropdown
124+
isOpen={openKebab === a.metadata.name}
125+
onOpenChange={(open) => setOpenKebab(open ? a.metadata.name : null)}
126+
toggle={(toggleRef) => (
127+
<MenuToggle
128+
ref={toggleRef}
129+
variant="plain"
130+
onClick={() =>
131+
setOpenKebab(openKebab === a.metadata.name ? null : a.metadata.name)
132+
}
133+
isExpanded={openKebab === a.metadata.name}
134+
>
135+
<EllipsisVIcon />
136+
</MenuToggle>
137+
)}
138+
popperProps={{ position: 'right' }}
139+
>
140+
<DropdownList>
141+
<DropdownItem key="delete" onClick={() => handleDelete(a)}>
142+
{t('Delete')}
143+
</DropdownItem>
144+
</DropdownList>
145+
</Dropdown>
146+
</Td>
147+
</Tr>
148+
))
149+
) : (
150+
<Tr>
151+
<Td colSpan={6}>{t('No agents found.')}</Td>
152+
</Tr>
153+
)}
154+
</Tbody>
155+
</Table>
156+
157+
{showForm && (
158+
<AgentForm
159+
providers={providers || []}
160+
onSubmit={handleCreate}
161+
onCancel={() => setShowForm(false)}
162+
/>
163+
)}
164+
</>
165+
);
166+
};
167+
168+
export default AgentsTab;

0 commit comments

Comments
 (0)