Skip to content

Commit 39754f4

Browse files
committed
Merge branch 'master' into chore/datetime-picker-e2e-tests
2 parents a942153 + 1023135 commit 39754f4

File tree

117 files changed

+24073
-609
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+24073
-609
lines changed

buf.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ deps:
1010
- name: buf.build/grpc-ecosystem/grpc-gateway
1111
commit: a48fcebcf8f140dd9d09359b9bb185a4
1212
digest: b5:330af8a71b579ab96c4f3ee26929d1a68a5a9e986c7cfe0a898591fc514216bb6e723dc04c74d90fdee3f3f14f9100a54b4f079eb273e6e7213f0d5baca36ff8
13-
- name: buf.build/opentelemetry/opentelemetry
14-
commit: 648a3e2f02e14fe187656ea4ac3befa1
15-
digest: b5:a0514be587ab2e8598f7102dfdaba5e985138b8c041c707e470a9c8b877410ada6e60cf2359352302f08c0504de685379f7f7a085d4699d69a2e6ed7035e78d9
13+
- name: buf.build/redpandadata/ai-gateway
14+
commit: 702ee5c67c994d9995e39e74bb6553f6
15+
digest: b5:9717dd679ced8e42eab232efcc89c4ebcf9334b5aa6664b502268d5c7d25766e2c23b201822383a0a7c943d663859c7054c0656937fce7b612f7b578a59e3eb2
1616
- name: buf.build/redpandadata/common
1717
commit: 601698cfe71d43b1b36fd434a7f765f1
1818
digest: b5:d566ba6746a874a5709970e7f8569584008447d655c556676aabd3430111bbbc0a303dde11d99a1955ee27ac4748bcf7c972503a66db4ef850eb55f239f851ac

buf.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ deps:
1111
- buf.build/redpandadata/common
1212
- buf.build/redpandadata/core:05aa34b3829a4d5a801d9487623a7c76
1313
- buf.build/redpandadata/otel
14+
- buf.build/redpandadata/ai-gateway
1415
lint:
1516
use:
1617
- STANDARD

frontend/bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/rsbuild.config.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,31 @@ export default defineConfig({
5353
origin: ['http://localhost:3000', 'http://localhost:9090'],
5454
credentials: true,
5555
},
56-
proxy: {
57-
context: ['/api', '/redpanda.api', '/auth', '/logout'],
58-
target: process.env.PROXY_TARGET || 'http://localhost:9090',
59-
changeOrigin: !!process.env.PROXY_TARGET,
60-
secure: process.env.PROXY_TARGET ? false : undefined,
61-
},
56+
proxy: [
57+
// AI Gateway API - proxy to separate AI Gateway service
58+
// Matches: /.redpanda/api/redpanda.api.aigateway.v1.*
59+
// Proto package is: redpanda.api.aigateway.v1 (includes .api)
60+
// AI Gateway now expects the full path with .api
61+
...(process.env.AI_GATEWAY_URL
62+
? [
63+
{
64+
context: ['/.redpanda/api/redpanda.api.aigateway.v1'],
65+
target: process.env.AI_GATEWAY_URL,
66+
changeOrigin: true,
67+
secure: false,
68+
logLevel: 'debug',
69+
// No pathRewrite - AI Gateway expects full path with .api
70+
},
71+
]
72+
: []),
73+
// All other APIs - proxy to Console backend
74+
{
75+
context: ['/api', '/redpanda.api', '/auth', '/logout'],
76+
target: process.env.PROXY_TARGET || 'http://localhost:9090',
77+
changeOrigin: !!process.env.PROXY_TARGET,
78+
secure: process.env.PROXY_TARGET ? false : undefined,
79+
},
80+
],
6281
},
6382
source: {
6483
define: {

frontend/src/app.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,22 @@ import { NotFoundPage } from './components/misc/not-found-page';
5151
import { addBearerTokenInterceptor, checkExpiredLicenseInterceptor, getGrpcBasePath, setup } from './config';
5252
import { routeTree } from './routeTree.gen';
5353

54+
// Create transport before router so loaders can use it
55+
const dataplaneTransport = createConnectTransport({
56+
baseUrl: getGrpcBasePath(''), // Embedded mode handles the path separately.
57+
interceptors: [addBearerTokenInterceptor, checkExpiredLicenseInterceptor],
58+
jsonOptions: {
59+
registry: protobufRegistry,
60+
},
61+
});
62+
5463
// Create router instance
5564
const router = createRouter({
5665
routeTree,
5766
context: {
5867
basePath: getBasePath(),
5968
queryClient,
69+
dataplaneTransport,
6070
},
6171
basepath: getBasePath(),
6272
trailingSlash: 'never',
@@ -85,14 +95,6 @@ const App = () => {
8595
const developerView = useDeveloperView();
8696
setup({});
8797

88-
const dataplaneTransport = createConnectTransport({
89-
baseUrl: getGrpcBasePath(''), // Embedded mode handles the path separately.
90-
interceptors: [addBearerTokenInterceptor, checkExpiredLicenseInterceptor],
91-
jsonOptions: {
92-
registry: protobufRegistry,
93-
},
94-
});
95-
9698
// Need to use CustomFeatureFlagProvider for completeness with EmbeddedApp
9799
return (
98100
<CustomFeatureFlagProvider initialFlags={{}}>

frontend/src/components/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const FEATURE_FLAGS = {
1818
enableMcpServiceAccount: false,
1919
enablePipelineServiceAccount: false,
2020
enableTranscriptsInConsole: false,
21+
enableApiKeyConfigurationAgent: false,
2122
shadowlinkCloudUi: false,
2223
enableNewTheme: false,
2324
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright 2026 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
import { Center, Heading, Image, Stack } from '@redpanda-data/ui';
13+
import { Link } from '@tanstack/react-router';
14+
15+
import errorBananaSlip from '../../assets/redpanda/ErrorBananaSlip.svg';
16+
17+
type NotFoundContentProps = {
18+
/** The type of resource that wasn't found (e.g., "AI Agent", "Shadow Link") */
19+
resourceType: string;
20+
/** The ID or name of the resource that wasn't found */
21+
resourceId?: string;
22+
/** The link to navigate back to (e.g., "/agents") */
23+
backLink?: string;
24+
/** The text for the back link (e.g., "Back to AI Agents") */
25+
backLinkText?: string;
26+
};
27+
28+
/**
29+
* Reusable component for displaying resource-specific 404 pages.
30+
* Used by route notFoundComponent handlers when a specific resource is not found.
31+
*/
32+
export const NotFoundContent = ({ resourceType, resourceId, backLink, backLinkText }: NotFoundContentProps) => {
33+
const message = resourceId ? `${resourceType} "${resourceId}" not found.` : `${resourceType} not found.`;
34+
35+
return (
36+
<Center data-testid="not-found-content" h="80vh">
37+
<Stack spacing={4} textAlign="center">
38+
<Image alt="Error" height="180px" src={errorBananaSlip} />
39+
<Heading as="h1" fontSize={32} variant="lg">
40+
{message}
41+
</Heading>
42+
{backLink ? (
43+
<Link className="text-base underline" data-testid="back-link" to={backLink}>
44+
{backLinkText ?? 'Go back'}
45+
</Link>
46+
) : null}
47+
</Stack>
48+
</Center>
49+
);
50+
};

frontend/src/components/pages/agents/create/ai-agent-create-page.tsx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
type ServiceAccountSelectorRef,
3333
} from 'components/ui/service-account/service-account-selector';
3434
import { TagsFieldList } from 'components/ui/tag/tags-field-list';
35+
import { isFeatureFlagEnabled } from 'config';
3536
import { Loader2 } from 'lucide-react';
3637
import { Scope } from 'protogen/redpanda/api/dataplane/v1/secret_pb';
3738
import {
@@ -51,6 +52,7 @@ import {
5152
import { useEffect, useMemo, useRef, useState } from 'react';
5253
import { Controller, useFieldArray, useForm } from 'react-hook-form';
5354
import { useCreateAIAgentMutation } from 'react-query/api/ai-agent';
55+
import { useListGatewaysQuery } from 'react-query/api/ai-gateway';
5456
import { useListMCPServersQuery } from 'react-query/api/remote-mcp';
5557
import { useCreateSecretMutation, useListSecretsQuery } from 'react-query/api/secret';
5658
import { toast } from 'sonner';
@@ -72,6 +74,38 @@ export const AIAgentCreatePage = () => {
7274
skipInvalidation: true,
7375
});
7476

77+
// Feature flag: when true, use legacy API key mode (hardcoded providers)
78+
const isLegacyApiKeyMode = isFeatureFlagEnabled('enableApiKeyConfigurationAgent');
79+
80+
// Gateway detection and list query (using v1 API from ai-gateway module)
81+
// Only fetch when NOT in legacy mode
82+
const { data: gatewaysData, isLoading: isLoadingGateways } = useListGatewaysQuery(
83+
{},
84+
{ enabled: !isLegacyApiKeyMode }
85+
);
86+
87+
const hasGatewayDeployed = useMemo(() => {
88+
if (isLegacyApiKeyMode || isLoadingGateways) {
89+
return false;
90+
}
91+
return Boolean(gatewaysData?.gateways && gatewaysData.gateways.length > 0);
92+
}, [isLegacyApiKeyMode, gatewaysData, isLoadingGateways]);
93+
94+
const availableGateways = useMemo(() => {
95+
if (isLegacyApiKeyMode || !gatewaysData?.gateways) {
96+
return [];
97+
}
98+
return gatewaysData.gateways.map((gw) => {
99+
// Extract gateway ID from name (format: "gateways/{gateway_id}")
100+
const gatewayId = gw.name.split('/').pop() || gw.name;
101+
return {
102+
id: gatewayId,
103+
displayName: gw.displayName,
104+
description: gw.description,
105+
};
106+
});
107+
}, [isLegacyApiKeyMode, gatewaysData]);
108+
75109
// Ref to ServiceAccountSelector to call createServiceAccount
76110
const serviceAccountSelectorRef = useRef<ServiceAccountSelectorRef>(null);
77111

@@ -107,6 +141,13 @@ export const AIAgentCreatePage = () => {
107141
}
108142
}, [displayName, form]);
109143

144+
// Auto-select first gateway when gateways are available (only if not in legacy mode)
145+
useEffect(() => {
146+
if (!isLegacyApiKeyMode && availableGateways.length > 0 && !form.getValues('gatewayId')) {
147+
form.setValue('gatewayId', availableGateways[0].id);
148+
}
149+
}, [isLegacyApiKeyMode, availableGateways, form]);
150+
110151
const {
111152
fields: tagFields,
112153
append: appendTag,
@@ -294,7 +335,9 @@ export const AIAgentCreatePage = () => {
294335
});
295336

296337
// Build provider configuration based on selected provider
297-
const apiKeyRef = `\${secrets.${values.apiKeySecret}}`;
338+
// When using gateway: api_key can be empty (proto has ignore = IGNORE_IF_ZERO_VALUE)
339+
// When not using gateway: api_key must reference a secret
340+
const apiKeyRef = values.apiKeySecret ? `\${secrets.${values.apiKeySecret}}` : '';
298341
let providerConfig: AIAgent_Provider;
299342

300343
switch (values.provider) {
@@ -464,7 +507,7 @@ export const AIAgentCreatePage = () => {
464507
</CardHeader>
465508
<CardContent>
466509
<LLMConfigSection
467-
availableGateways={[]}
510+
availableGateways={availableGateways}
468511
availableSecrets={availableSecrets}
469512
fieldNames={{
470513
provider: 'provider',
@@ -475,7 +518,8 @@ export const AIAgentCreatePage = () => {
475518
gatewayId: 'gatewayId',
476519
}}
477520
form={form}
478-
hasGatewayDeployed={false}
521+
hasGatewayDeployed={hasGatewayDeployed}
522+
isLoadingGateways={isLoadingGateways}
479523
mode="create"
480524
scopes={[Scope.MCP_SERVER, Scope.AI_AGENT]}
481525
showBaseUrl={form.watch('provider') === 'openaiCompatible'}

frontend/src/components/pages/agents/create/schemas.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ export const FormSchema = z
4949
{ message: 'Tags must have unique keys' }
5050
),
5151
triggerType: z.enum(['http', 'slack', 'kafka']).default('http'),
52+
gatewayId: z
53+
.string()
54+
.refine(
55+
(val) => !val || (val.length === 20 && /^[a-z0-9]+$/.test(val)),
56+
'Gateway ID must be exactly 20 lowercase alphanumeric characters'
57+
)
58+
.optional()
59+
.or(z.literal('')),
5260
provider: z.enum(['openai', 'anthropic', 'google', 'openaiCompatible']).default('openai'),
5361
apiKeySecret: z.string(),
5462
model: z.string().min(1, 'Model is required'),
@@ -77,15 +85,24 @@ export const FormSchema = z
7785
},
7886
{ message: 'Subagent names must be unique' }
7987
),
80-
gatewayId: z
81-
.string()
82-
.length(20, 'Gateway ID must be exactly 20 characters')
83-
.regex(/^[a-z0-9]+$/, 'Gateway ID must contain only lowercase letters and numbers')
84-
.optional()
85-
.or(z.literal('')),
8688
})
8789
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex validation logic with multiple conditional checks
8890
.superRefine((data, ctx) => {
91+
// Note: Gateway validation happens in the UI layer based on availability
92+
// If gateways are available, gateway is required (enforced by UI)
93+
// If gateways are NOT available, API key is required
94+
95+
const hasGateway = data.gatewayId && data.gatewayId.trim() !== '';
96+
97+
if (!hasGateway && (!data.apiKeySecret || data.apiKeySecret.trim() === '')) {
98+
// No gateway selected: API Key is required
99+
ctx.addIssue({
100+
code: z.ZodIssueCode.custom,
101+
message: 'API Token is required',
102+
path: ['apiKeySecret'],
103+
});
104+
}
105+
89106
if (data.provider === 'openaiCompatible') {
90107
if (!data.baseUrl || data.baseUrl.trim() === '') {
91108
ctx.addIssue({
@@ -131,6 +148,7 @@ export const initialValues: FormValues = {
131148
description: '',
132149
tags: [],
133150
triggerType: 'http',
151+
gatewayId: '',
134152
provider: 'openai',
135153
apiKeySecret: '',
136154
model: '',
@@ -142,5 +160,4 @@ export const initialValues: FormValues = {
142160
systemPrompt: '',
143161
serviceAccountName: '',
144162
subagents: [],
145-
gatewayId: '',
146163
};

frontend/src/components/pages/agents/details/a2a/chat/components/chat-message.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import { Message, MessageBody, MessageContent, MessageMetadata } from 'components/ai-elements/message';
1313

1414
import { ChatMessageActions } from './chat-message-actions';
15+
import { A2AErrorBlock } from './message-blocks/a2a-error-block';
1516
import { ArtifactBlock } from './message-blocks/artifact-block';
1617
import { TaskStatusUpdateBlock } from './message-blocks/task-status-update-block';
1718
import { ToolBlock } from './message-blocks/tool-block';
@@ -101,6 +102,8 @@ export const ChatMessage = ({ message, isLoading: _isLoading }: ChatMessageProps
101102
timestamp={block.timestamp}
102103
/>
103104
);
105+
case 'a2a-error':
106+
return <A2AErrorBlock error={block.error} key={`${message.id}-error-${index}`} timestamp={block.timestamp} />;
104107
default:
105108
return null;
106109
}

0 commit comments

Comments
 (0)