mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19ad8d6806 | ||
|
|
38fc2970e6 | ||
|
|
4ec4d29e84 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import SquareIcon from "@components/SquareIcon";
|
||||
import AddPeerButton from "@components/ui/AddPeerButton";
|
||||
import AddPeerDropdown from "@components/ui/AddPeerDropdown";
|
||||
import GetStartedTest from "@components/ui/GetStartedTest";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
@@ -8,17 +8,10 @@ import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
|
||||
type Props = {
|
||||
showBackground?: boolean;
|
||||
// When set, tailors the empty-state copy and threads isUserDevice
|
||||
// through AddPeerButton so the right Install NetBird flow opens:
|
||||
// true → User Devices empty state (browser/SSO flow, mobile tabs).
|
||||
// false → Servers empty state (setup-key flow, no mobile tabs).
|
||||
// undefined → legacy/global empty state (no kind preference).
|
||||
isUserDevice?: boolean;
|
||||
};
|
||||
|
||||
export const NoPeersGettingStarted = ({
|
||||
showBackground = true,
|
||||
isUserDevice,
|
||||
}: Readonly<Props>) => {
|
||||
return (
|
||||
<GetStartedTest
|
||||
@@ -35,7 +28,7 @@ export const NoPeersGettingStarted = ({
|
||||
"It looks like you don't have any connected machines.\n" +
|
||||
"Get started by adding one to your network."
|
||||
}
|
||||
button={<AddPeerButton isUserDevice={isUserDevice} />}
|
||||
button={<AddPeerDropdown />}
|
||||
learnMore={
|
||||
<>
|
||||
Learn more in our{" "}
|
||||
|
||||
@@ -28,8 +28,9 @@ import {
|
||||
PlusCircle,
|
||||
PlusIcon,
|
||||
Sparkles,
|
||||
UploadIcon,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import AgentNetworkIcon from "@/assets/icons/AgentNetworkIcon";
|
||||
import {
|
||||
ReverseProxyDomain,
|
||||
@@ -100,6 +101,8 @@ function upstreamUrlPlaceholder(providerId: AIProviderId): string {
|
||||
return "https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai";
|
||||
case "vercel_ai_gateway":
|
||||
return "https://ai-gateway.vercel.sh";
|
||||
case "vertex_ai_api":
|
||||
return "https://aiplatform.googleapis.com";
|
||||
case "openrouter":
|
||||
return "https://openrouter.ai/api/v1";
|
||||
case "litellm_proxy":
|
||||
@@ -168,6 +171,25 @@ export default function AIProviderModal({
|
||||
const [apiKey, setApiKey] = useState(isEdit ? "••••••••" : "");
|
||||
const [bootstrapCluster, setBootstrapCluster] = useState<string>("");
|
||||
const [models, setModels] = useState<ProviderModel[]>(provider?.models ?? []);
|
||||
|
||||
// Vertex AI authenticates with a service-account JSON key, not an API key.
|
||||
// We upload the file and store it base64-encoded in apiKey (the server
|
||||
// decodes the base64 JSON); keyFileName tracks the uploaded file for display.
|
||||
const [keyFileName, setKeyFileName] = useState<string | null>(null);
|
||||
const keyFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onJsonKeyUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
let binary = "";
|
||||
bytes.forEach((b) => (binary += String.fromCharCode(b)));
|
||||
// The server expects the JSON key prefixed with "keyfile::".
|
||||
setApiKey("keyfile::" + window.btoa(binary));
|
||||
setKeyFileName(file.name);
|
||||
// Clear so picking the same file again still fires onChange.
|
||||
e.target.value = "";
|
||||
};
|
||||
// extraValues holds operator-typed values for catalog-declared extra
|
||||
// headers (e.g. Portkey's x-portkey-config). Keyed by wire header
|
||||
// name. Catalog (catalog.extra_headers) decides which inputs render
|
||||
@@ -267,6 +289,9 @@ export default function AIProviderModal({
|
||||
if (upstreamUrl) return;
|
||||
if (!catalog) return;
|
||||
if (!catalog.default_host) return;
|
||||
// Vertex's catalog host is templated (<region>-…); skip the prefill so the
|
||||
// clean https://aiplatform.googleapis.com placeholder shows instead.
|
||||
if (catalog.id === "vertex_ai_api") return;
|
||||
setUpstreamUrl(`https://${catalog.default_host}`);
|
||||
}, [provider, upstreamUrl, catalog]);
|
||||
|
||||
@@ -519,6 +544,10 @@ export default function AIProviderModal({
|
||||
onChange={(v) => {
|
||||
const next = v as AIProviderId;
|
||||
setProviderId(next);
|
||||
// The credential differs per provider (API key vs Vertex
|
||||
// JSON upload), so clear it when switching.
|
||||
setApiKey("");
|
||||
setKeyFileName(null);
|
||||
const c = getById(next);
|
||||
if (c) {
|
||||
setName(c.name);
|
||||
@@ -527,7 +556,11 @@ export default function AIProviderModal({
|
||||
// endpoint). Don't pre-fill "https://" — let the
|
||||
// placeholder hint them what to type instead.
|
||||
setUpstreamUrl(
|
||||
c.default_host ? `https://${c.default_host}` : "",
|
||||
next === "vertex_ai_api"
|
||||
? "https://aiplatform.googleapis.com"
|
||||
: c.default_host
|
||||
? `https://${c.default_host}`
|
||||
: "",
|
||||
);
|
||||
setModels([]);
|
||||
// Auto-seed the identity inputs from the
|
||||
@@ -571,41 +604,89 @@ export default function AIProviderModal({
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
label={
|
||||
<>
|
||||
Provider API key
|
||||
<HelpTooltip
|
||||
content={
|
||||
<>
|
||||
NetBird injects it as{" "}
|
||||
<code className={"text-nb-gray-200"}>
|
||||
{catalog?.auth_header_template}
|
||||
</code>{" "}
|
||||
on every upstream request, so agents never see the
|
||||
key.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
helpText={"The API key issued by the provider."}
|
||||
>
|
||||
<Input
|
||||
type={"password"}
|
||||
showPasswordToggle
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
customPrefix={<KeyRound size={14} />}
|
||||
placeholder={
|
||||
providerId === "openai_api"
|
||||
? "sk-..."
|
||||
: providerId === "anthropic_api"
|
||||
? "sk-ant-..."
|
||||
: "Paste your API key"
|
||||
{providerId === "vertex_ai_api" ? (
|
||||
<FormRow
|
||||
label={
|
||||
<>
|
||||
Service account JSON key
|
||||
<HelpTooltip
|
||||
content={
|
||||
<>
|
||||
Upload the Vertex AI service account JSON key.
|
||||
NetBird base64-encodes it and prefixes it with{" "}
|
||||
<code className={"text-nb-gray-200"}>keyfile::</code>{" "}
|
||||
before injecting it on every upstream request, so
|
||||
agents never see the key.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</FormRow>
|
||||
helpText={"Upload the service account JSON key file."}
|
||||
>
|
||||
<div className={"flex items-center gap-3"}>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => keyFileInputRef.current?.click()}
|
||||
>
|
||||
<UploadIcon size={14} />
|
||||
{keyFileName || (isEdit && apiKey === "••••••••")
|
||||
? "Replace JSON key"
|
||||
: "Upload JSON key"}
|
||||
</Button>
|
||||
<span className={"text-xs text-nb-gray-300 truncate"}>
|
||||
{keyFileName
|
||||
? keyFileName
|
||||
: isEdit && apiKey === "••••••••"
|
||||
? "A key is already stored"
|
||||
: "No file selected"}
|
||||
</span>
|
||||
<input
|
||||
ref={keyFileInputRef}
|
||||
type={"file"}
|
||||
accept={"application/json,.json"}
|
||||
className={"hidden"}
|
||||
onChange={onJsonKeyUpload}
|
||||
/>
|
||||
</div>
|
||||
</FormRow>
|
||||
) : (
|
||||
<FormRow
|
||||
label={
|
||||
<>
|
||||
Provider API key
|
||||
<HelpTooltip
|
||||
content={
|
||||
<>
|
||||
NetBird injects it as{" "}
|
||||
<code className={"text-nb-gray-200"}>
|
||||
{catalog?.auth_header_template}
|
||||
</code>{" "}
|
||||
on every upstream request, so agents never see the
|
||||
key.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
helpText={"The API key issued by the provider."}
|
||||
>
|
||||
<Input
|
||||
type={"password"}
|
||||
showPasswordToggle
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
customPrefix={<KeyRound size={14} />}
|
||||
placeholder={
|
||||
providerId === "openai_api"
|
||||
? "sk-..."
|
||||
: providerId === "anthropic_api"
|
||||
? "sk-ant-..."
|
||||
: "Paste your API key"
|
||||
}
|
||||
/>
|
||||
</FormRow>
|
||||
)}
|
||||
{(catalog?.extra_headers ?? []).map((h) => {
|
||||
const ui = EXTRA_HEADER_UI[h.name] ?? fallbackExtraHeaderUI(h.name);
|
||||
return (
|
||||
|
||||
@@ -74,6 +74,9 @@ export function AgentConnectTabs({
|
||||
<Tabs key={defaultTab} defaultValue={defaultTab} className={"mt-2"}>
|
||||
<TabsList justify={"start"} className={listClassName}>
|
||||
<TabsTrigger value={"claude-code"}>Claude Code</TabsTrigger>
|
||||
<TabsTrigger value={"claude-code-vertex"}>
|
||||
Claude Code (Vertex AI)
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"codex"}>Codex</TabsTrigger>
|
||||
<TabsTrigger value={"openai-sdk"}>OpenAI SDK</TabsTrigger>
|
||||
<TabsTrigger value={"curl"}>cURL</TabsTrigger>
|
||||
@@ -120,6 +123,25 @@ export function AgentConnectTabs({
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"claude-code-vertex"}>
|
||||
<div className={contentClassName}>
|
||||
<Snippet
|
||||
caption={"Add to ~/.claude/settings.json:"}
|
||||
lines={[
|
||||
`{`,
|
||||
` "env": {`,
|
||||
` "CLOUD_ML_REGION": "global",`,
|
||||
` "ANTHROPIC_VERTEX_PROJECT_ID": "<your-gcp-project-id>",`,
|
||||
` "CLAUDE_CODE_USE_VERTEX": "1",`,
|
||||
` "CLAUDE_CODE_SKIP_VERTEX_AUTH": "1",`,
|
||||
` "ANTHROPIC_VERTEX_BASE_URL": "${baseUrl}/v1"`,
|
||||
` }`,
|
||||
`}`,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"codex"}>
|
||||
<div className={contentClassName}>
|
||||
<Snippet
|
||||
|
||||
@@ -518,10 +518,7 @@ export default function PeersTable({
|
||||
// still shows.
|
||||
hasServerSideFilters={kind !== undefined && (peers?.length ?? 0) > 0}
|
||||
getStartedCard={
|
||||
<NoPeersGettingStarted
|
||||
showBackground={true}
|
||||
isUserDevice={kind ? kind === "users" : undefined}
|
||||
/>
|
||||
<NoPeersGettingStarted showBackground={true} />
|
||||
}
|
||||
rightSide={() => (
|
||||
<>
|
||||
|
||||
Reference in new issue
Block a user