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
10 changes: 10 additions & 0 deletions config/ai_connectors.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ func RenderManagedAIConnectorsSection(connectors []uicfg.ConnectorRemote, sectio
builder.WriteString(strconv.Quote(connector.BaseURL))
builder.WriteString("\n")
}
if connector.GCPProjectID != "" {
builder.WriteString("gcp_project_id = ")
builder.WriteString(strconv.Quote(connector.GCPProjectID))
builder.WriteString("\n")
}
if connector.GCPLocation != "" {
builder.WriteString("gcp_location = ")
builder.WriteString(strconv.Quote(connector.GCPLocation))
builder.WriteString("\n")
}
if connector.SelectedModel != "" {
builder.WriteString("selected_model = ")
builder.WriteString(strconv.Quote(connector.SelectedModel))
Expand Down
16 changes: 16 additions & 0 deletions internal/staticserve/static/ui-connectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,8 @@ function App() {
api_key: connector.api_key || '',
base_url: connector.base_url || '',
selected_model: connector.selected_model || '',
gcp_project_id: connector.gcp_project_id || '',
gcp_location: connector.gcp_location || '',
});
setStatus(`Editing connector #${connector.id}`);
}
Expand All @@ -398,6 +400,8 @@ function App() {
base_url: provider.requiresBaseURL
? (provider.baseURLPresets?.length ? provider.baseURLPresets[0].value : '')
: '',
gcp_project_id: '',
gcp_location: '',
}));
if (provider.id !== 'ollama') {
setOllamaModels([]);
Expand Down Expand Up @@ -426,6 +430,8 @@ function App() {
api_key: connector.api_key || '',
base_url: connector.base_url || '',
selected_model: connector.selected_model || '',
gcp_project_id: connector.gcp_project_id || '',
gcp_location: connector.gcp_location || '',
});
setStatus(`Editing connector #${connector.id}`);
navigate(`/connectors/edit/${connector.id}`);
Expand Down Expand Up @@ -531,6 +537,8 @@ function App() {
api_key: apiKey,
base_url: baseURL || undefined,
model: selectedModel || apiDefaultModel || undefined,
gcp_project_id: (form.gcp_project_id || '').trim() || undefined,
gcp_location: (form.gcp_location || '').trim() || undefined,
}),
});

Expand All @@ -547,6 +555,8 @@ function App() {
connector_name: connectorName,
base_url: baseURL || undefined,
selected_model: selectedModel || apiDefaultModel || undefined,
gcp_project_id: (form.gcp_project_id || '').trim() || undefined,
gcp_location: (form.gcp_location || '').trim() || undefined,
display_order: 0,
};

Expand Down Expand Up @@ -663,6 +673,12 @@ function App() {
return !baseURL || !selectedModel;
}

if (provider.id === 'gemini-enterprise') {
const gcpProjectID = (form.gcp_project_id || '').trim();
const gcpLocation = (form.gcp_location || '').trim();
return !apiKey || !gcpProjectID || !gcpLocation;
}

if (!apiKey) {
return true;
}
Expand Down
125 changes: 113 additions & 12 deletions internal/staticserve/static/ui-connectors/pages/ConnectorFormPage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { html } = window.preact;
const { html, useState, useEffect } = window.preact;

export function ConnectorFormPage({
title,
Expand Down Expand Up @@ -27,7 +27,15 @@ export function ConnectorFormPage({
connectorNamePlaceholder,
apiDefaultModel = '',
}) {
const [fileError, setFileError] = useState('');

// Clear file error when provider changes
useEffect(() => {
setFileError('');
}, [form.provider_name]);

const isOllama = form.provider_name === 'ollama';
const isGeminiEnterprise = form.provider_name === 'gemini-enterprise';
const showBaseURL = Boolean(selectedProvider.requiresBaseURL);
const connectorName = (form.connector_name || '').trim();
const apiKey = (form.api_key || '').trim();
Expand All @@ -43,9 +51,40 @@ export function ConnectorFormPage({
}
}

const handleFileChange = (e) => {
const file = e.target.files?.[0];
if (!file) return;

if (file.size > 1024 * 1024) {
setFileError('File is too large. Please upload a valid service account JSON under 1MB.');
onFieldChange('api_key', '');
return;
}

const reader = new FileReader();
reader.onload = (event) => {
const content = event.target.result;
onFieldChange('api_key', content);

try {
const parsed = JSON.parse(content);
if (parsed.project_id) {
onFieldChange('gcp_project_id', parsed.project_id);
}
setFileError('');
} catch (err) {
console.error('Failed to parse Service Account JSON:', err);
setFileError('Failed to parse Service Account JSON. Please upload a valid JSON file.');
onFieldChange('api_key', '');
}
};
reader.readAsText(file);
};

const hasClientValidationError =
!connectorName ||
(!isOllama && !apiKey) ||
(isGeminiEnterprise && (!apiKey || !form.gcp_project_id || !form.gcp_location)) ||
(!isOllama && !isGeminiEnterprise && !apiKey) ||
(showBaseURL && (!baseURL || !hasValidBaseURL));

const fetchModelsDisabled = fetchingModels || !baseURL || !hasValidBaseURL;
Expand Down Expand Up @@ -74,16 +113,78 @@ export function ConnectorFormPage({
</button>
</div>

<label>${isOllama ? 'JWT Token (optional)' : 'API Key'}</label>
<input
type="password"
value=${form.api_key}
required=${!isOllama}
autoComplete="new-password"
spellcheck="false"
placeholder=${selectedProvider.apiKeyPlaceholder || ''}
onInput=${(event) => onFieldChange('api_key', event.target.value)}
/>
${isGeminiEnterprise
? html`
<label>Service Account JSON</label>
<div class="file-upload-container" style="display: flex; align-items: center; justify-content: space-between; border: 1px dashed var(--border-medium, #454545); background: var(--bg-tertiary, #2d2d30); border-radius: 6px; padding: 16px; margin-bottom: 15px;">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="padding: 8px; background: rgba(0, 122, 204, 0.1); border-radius: 6px; color: var(--text-link, #007acc); display: flex; align-items: center; justify-content: center;">
<svg style="width: 24px; height: 24px;" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div>
<h4 style="margin: 0; font-size: 14px; font-weight: 500; color: var(--text-primary, #cccccc);">
${form.api_key ? "Service Account JSON Loaded" : "Upload Credentials File"}
</h4>
<p style="margin: 4px 0 0 0; font-size: 12px; color: var(--text-muted, #858585);">
${form.api_key
? `Valid Google Cloud service account JSON configured (${(form.api_key.length / 1024).toFixed(2)} KB)`
: "Upload the Google Cloud IAM Service Account JSON keyfile"
}
</p>
</div>
</div>
<div>
<label class="cursor-pointer" style="cursor: pointer;">
<span style="display: inline-flex; align-items: center; padding: 6px 12px; border: 1px solid var(--border-medium, #454545); background: transparent; font-size: 13px; font-weight: 500; border-radius: 4px; color: var(--text-link, #007acc); transition: all 0.2s;">
${form.api_key ? "Replace File" : "Choose File"}
</span>
<input
type="file"
accept=".json"
onChange=${handleFileChange}
style="display: none;"
/>
</label>
</div>
</div>
${fileError ? html`<div class="status err" style="margin-top: -10px; margin-bottom: 15px;">${fileError}</div>` : ''}
<p style="margin: -5px 0 15px 0; font-size: 12px; color: var(--text-muted, #858585); line-height: 1.5;">
Follow this guide to <a href="https://developers.google.com/workspace/guides/create-credentials#service-account" target="_blank" rel="noopener noreferrer" style="color: var(--text-link, #007acc); text-decoration: underline;">create a service account JSON file</a> and assign the <strong>Agent Platform user</strong> role.
</p>

<label>GCP Project ID</label>
<input
type="text"
required
placeholder="e.g. my-gcp-project-id"
value=${form.gcp_project_id || ''}
onInput=${(event) => onFieldChange('gcp_project_id', event.target.value)}
/>

<label>GCP Location (Region)</label>
<input
type="text"
required
placeholder="e.g. us-central1, europe-west9"
value=${form.gcp_location || ''}
onInput=${(event) => onFieldChange('gcp_location', event.target.value)}
/>
`
: html`
<label>${isOllama ? 'JWT Token (optional)' : 'API Key'}</label>
<input
type="password"
value=${form.api_key}
required=${!isOllama}
autoComplete="new-password"
spellcheck="false"
placeholder=${selectedProvider.apiKeyPlaceholder || ''}
onInput=${(event) => onFieldChange('api_key', event.target.value)}
/>
`
}

${showBaseURL
? (() => {
Expand Down
7 changes: 7 additions & 0 deletions internal/staticserve/static/ui-connectors/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export const providers = [
name: 'Atlas Cloud',
apiKeyPlaceholder: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
baseURLPlaceholder: 'https://api.atlascloud.ai/v1',
},
{
id: 'gemini-enterprise',
name: 'Gemini Enterprise',
apiKeyPlaceholder: 'Google Cloud Service Account JSON',
}
];

Expand All @@ -75,5 +80,7 @@ export function defaultForm() {
api_key: '',
base_url: '',
selected_model: '',
gcp_project_id: '',
gcp_location: '',
};
}
2 changes: 2 additions & 0 deletions ui/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type ConnectorRemote struct {
ConnectorName string `json:"connector_name"`
APIKey string `json:"api_key"`
BaseURL string `json:"base_url"`
GCPProjectID string `json:"gcp_project_id"`
GCPLocation string `json:"gcp_location"`
SelectedModel string `json:"selected_model"`
DisplayOrder int `json:"display_order"`
CreatedAt string `json:"created_at"`
Expand Down
Loading