first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
import { ShieldCheck } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function AccessProfilesIndexView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<ShieldCheck className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock access profiles for better performance"
description="This feature is a part of the Bifrost enterprise license. Create access profiles to control access to your resources."
readmeLink="https://docs.getbifrost.ai/enterprise/access-profiles"
testIdPrefix="access-profiles"
/>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { Shuffle } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function AdaptiveRoutingView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<Shuffle className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock adaptive routing for better performance"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/adaptive-load-balancing"
/>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { Siren } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function AlertChannelsView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<Siren className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock alert channels for better observability"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/alert-channels"
/>
</div>
);
}

View File

@@ -0,0 +1,104 @@
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { useGetCoreConfigQuery } from "@/lib/store";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
import { Link } from "@tanstack/react-router";
import { Copy, InfoIcon, KeyRound } from "lucide-react";
import { useMemo } from "react";
import ContactUsView from "../views/contactUsView";
export default function APIKeysView() {
const { data: bifrostConfig, isLoading } = useGetCoreConfigQuery({ fromDB: true });
const isAuthConfigure = useMemo(() => {
return bifrostConfig?.auth_config?.is_enabled;
}, [bifrostConfig]);
const curlExample = `# Base64 encode your username:password
# Example: echo -n "username:password" | base64
curl --location 'http://localhost:8080/v1/chat/completions'
--header 'Content-Type: application/json'
--header 'Accept: application/json'
--header 'Authorization: Basic <base64_encoded_username:password>'
--data '{
"model": "openai/gpt-4",
"messages": [
{
"role": "user",
"content": "explain big bang?"
}
]
}'`;
const { copy: copyToClipboard } = useCopyToClipboard();
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthConfigure) {
return (
<Alert variant="default">
<InfoIcon className="text-muted h-4 w-4" />
<AlertDescription>
<p className="text-md text-muted-foreground">
To generate API keys, you need to set up admin username and password first.{" "}
<Link to="/workspace/config/security" className="text-md text-primary underline">
Configure Security Settings
</Link>
.<br />
<br />
Once generated you will need to use this API key for all API calls to the Bifrost admin APIs and UI.
</p>
</AlertDescription>
</Alert>
);
}
const isInferenceAuthDisabled = bifrostConfig?.auth_config?.disable_auth_on_inference ?? false;
return (
<div className="mx-auto w-full max-w-4xl space-y-4">
<Alert variant="default">
<InfoIcon className="text-muted h-4 w-4" />
<AlertDescription>
<p className="text-md text-muted-foreground">
{isInferenceAuthDisabled ? (
<>
Authentication is currently <strong>disabled for inference API calls</strong>. You can make inference requests without
authentication. Dashboard and admin API calls still require Basic auth with your admin credentials encoded in the standard{" "}
<code className="bg-muted rounded px-1 py-0.5 text-sm">username:password</code> format with base64 encoding.
</>
) : (
<>
Use Basic auth with your admin credentials when making API calls to Bifrost. Encode your credentials in the standard{" "}
<code className="bg-muted rounded px-1 py-0.5 text-sm">username:password</code> format with base64 encoding.
</>
)}
</p>
{!isInferenceAuthDisabled && (
<>
<br />
<p className="text-md text-muted-foreground">
<strong>Example:</strong>
</p>
<div className="relative mt-2 w-full min-w-0 overflow-x-auto">
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(curlExample)} className="absolute top-2 right-2 z-10 h-8">
<Copy className="h-4 w-4" />
</Button>
<pre className="bg-muted min-w-max rounded p-3 pr-12 font-mono text-sm whitespace-pre">{curlExample}</pre>
</div>
</>
)}
</AlertDescription>
</Alert>
<ContactUsView
className="mt-4 rounded-md border px-3 py-8"
icon={<KeyRound size={48} />}
title="Scope Based API Keys"
description="Need granular access control with scope-based API keys? Enterprise customers can create multiple API keys with specific permissions for different services, teams, or environments."
readmeLink="https://docs.getbifrost.io/enterprise/api-keys"
/>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { ScrollText } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function AuditLogsView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<ScrollText className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock audit logs for better compliance"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/audit-logs"
/>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { Layers } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function ClusterPage() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<Layers className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock cluster mode to scale reliably"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/clustering"
/>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { Database } from "lucide-react";
import ContactUsView from "../../views/contactUsView";
interface EnableToggleProps {
enabled: boolean;
onToggle: () => void;
disabled?: boolean;
}
interface BigQueryConnectorViewProps {
onDelete?: () => void;
isDeleting?: boolean;
enableToggle?: EnableToggleProps;
}
export default function BigQueryConnectorView(_props: BigQueryConnectorViewProps) {
return (
<div className="space-y-6">
{/* Content - OSS: paywall only; no delete/save buttons */}
<div className="space-y-4">
<div className="flex w-full flex-col items-center justify-center py-8">
<ContactUsView
align="middle"
className="mx-auto w-full max-w-lg"
icon={<Database className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock native BigQuery data ingestion for analytics"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/bigquery-connector"
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { Dog } from "lucide-react";
import ContactUsView from "../../views/contactUsView";
interface EnableToggleProps {
enabled: boolean;
onToggle: () => void;
disabled?: boolean;
}
interface DatadogConnectorViewProps {
onDelete?: () => void;
isDeleting?: boolean;
enableToggle?: EnableToggleProps;
}
export default function DatadogConnectorView(_props: DatadogConnectorViewProps) {
return (
<div className="space-y-6">
{/* Content - OSS: paywall only; no delete/save buttons */}
<div className="space-y-4">
<div className="flex w-full flex-col items-center justify-center py-8">
<ContactUsView
align="middle"
className="mx-auto w-full max-w-lg"
icon={<Dog className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock native Datadog data ingestion for better observability"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/datadog-connector"
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { Construction } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function GuardrailsConfigurationView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<Construction className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock guardrails for better security"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/guardrails"
/>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { Construction } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function guardrailsProviderView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<Construction className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock guardrails for better security"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/guardrails"
/>
</div>
);
}

View File

@@ -0,0 +1,11 @@
import { LargePayloadConfig } from "@enterprise/lib/types/largePayload";
export interface LargePayloadSettingsFragmentProps {
config: LargePayloadConfig;
onConfigChange: (config: LargePayloadConfig) => void;
controlsDisabled: boolean;
}
export default function LargePayloadSettingsFragment(_props: LargePayloadSettingsFragmentProps) {
return null;
}

View File

@@ -0,0 +1,186 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { getErrorMessage, useIsAuthEnabledQuery, useLoginMutation } from "@/lib/store/apis";
import { BooksIcon, DiscordLogoIcon, GithubLogoIcon } from "@phosphor-icons/react";
import { useNavigate } from "@tanstack/react-router";
import { Eye, EyeOff } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
const externalLinks = [
{
title: "Discord Server",
url: "https://discord.gg/exN5KAydbU",
icon: DiscordLogoIcon,
},
{
title: "GitHub Repository",
url: "https://github.com/maximhq/bifrost",
icon: GithubLogoIcon,
},
{
title: "Full Documentation",
url: "https://docs.getbifrost.ai",
icon: BooksIcon,
strokeWidth: 1,
},
];
export default function LoginView() {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const { data: isAuthEnabledData, isLoading: isLoadingIsAuthEnabled, error: isAuthEnabledError } = useIsAuthEnabledQuery();
const isAuthEnabled = isAuthEnabledData?.is_auth_enabled || false;
const hasValidToken = isAuthEnabledData?.has_valid_token || false;
const [login, { isLoading: isLoggingIn }] = useLoginMutation();
useEffect(() => {
setMounted(true);
}, []);
// Check auth status on component mount
useEffect(() => {
if (isLoadingIsAuthEnabled) {
return;
}
if (isAuthEnabledError) {
setErrorMessage("Unable to verify authentication status. Please retry.");
return;
}
if (!isAuthEnabled || hasValidToken) {
navigate({ to: "/workspace" });
return;
}
// Auth is enabled but user is not logged in, show login form
setIsCheckingAuth(false);
}, [isLoadingIsAuthEnabled]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
setIsLoading(true);
e.preventDefault();
setErrorMessage("");
try {
await login({ username, password }).unwrap();
// Cookie is set automatically by the server response — just navigate
navigate({ to: "/workspace" });
} catch (error) {
const message = getErrorMessage(error);
setErrorMessage(message);
} finally {
setIsLoading(false);
}
};
// Use light logo for SSR to avoid hydration mismatch
const logoSrc = mounted && resolvedTheme === "dark" ? "/bifrost-logo-dark.webp" : "/bifrost-logo.webp";
// Show loading state while checking auth
if (isCheckingAuth || isLoadingIsAuthEnabled) {
return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="border-border bg-card w-full space-y-6 rounded-sm border p-8">
<div className="flex items-center justify-center">
<img src={logoSrc} alt="Bifrost" width={160} height={26} className="" />
</div>
<div className="flex items-center justify-center py-8">
<div className="text-muted-foreground text-sm">Checking authentication...</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="border-border bg-card w-full space-y-6 rounded-sm border p-8">
{/* Logo */}
<div className="flex items-center justify-center">
<img src={logoSrc} alt="Bifrost" width={160} height={26} className="" />
</div>
<div className="space-y-2 text-center">
<h1 className="text-foreground text-lg font-semibold">Welcome back</h1>
<p className="text-muted-foreground text-sm">Sign in to your account to continue</p>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{errorMessage && <div className="bg-destructive/10 text-destructive rounded-sm p-3 text-sm">{errorMessage}</div>}
<div className="space-y-2">
<Label htmlFor="username" className="text-sm font-medium">
Username
</Label>
<Input
id="username"
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
className="text-sm"
autoComplete="username"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm font-medium">
Password
</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="pr-10 text-sm"
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-3 -translate-y-1/2 transition-colors"
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<Button type="submit" className="h-9 w-full text-sm" isLoading={isLoading} disabled={isLoading}>
{isLoading || isLoggingIn ? "Signing in..." : "Sign in"}
</Button>
</form>
{/* Social Links */}
<div className="flex items-center justify-center gap-4 pt-4">
{externalLinks.map((item, index) => (
<a
key={index}
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary transition-colors"
title={item.title}
>
<item.icon className="h-5 w-5" size={20} weight="regular" strokeWidth={item.strokeWidth} />
</a>
))}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { ShieldUser } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function MCPAuthConfigView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<ShieldUser className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock MCP Auth Config"
description="This feature is a part of the Bifrost enterprise license. Configure authentication for MCP servers to secure your MCP connections."
readmeLink="https://docs.getbifrost.ai/mcp/overview"
/>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import { ToolCase } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function MCPToolGroups() {
return (
<>
<div className="flex items-center justify-between gap-4 mb-4">
<div>
<h2 className="text-lg font-semibold tracking-tight">MCP tool groups</h2>
<p className="text-muted-foreground text-sm">Configure tool groups for MCP servers to organize and govern tools.</p>
</div>
</div>
<div className="rounded-sm border">
<div className="flex w-full flex-col items-center justify-center py-16">
<ContactUsView
className="mx-auto w-full max-w-lg"
icon={<ToolCase className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock MCP Tool Groups"
description="This feature is a part of the Bifrost enterprise license. Configure tool groups for MCP servers to organize your MCP tools and govern them across your organization."
readmeLink="https://docs.getbifrost.ai/mcp/overview"
/>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,16 @@
import { ScanEye } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function PiiRedactorProviderView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<ScanEye className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock PII Redaction for better privacy"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/pii-redactor"
/>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { ScanEye } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function PiiRedactorRulesView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<ScanEye className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock PII Redaction for better privacy"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/pii-redactor"
/>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { Router } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function PromptDeploymentView(_props?: { omitTitle?: boolean }) {
return (
<div className="w-full">
<ContactUsView
align="top"
className="justify-start gap-3 rounded-md border p-4"
icon={<Router className="h-8 w-8" strokeWidth={1.5} />}
title="Unlock prompt deployments for better prompt versioning and A/B testing."
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/prompt-deployments"
/>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { usePromptContext } from "@/components/prompts/context";
import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { cn } from "@/lib/utils";
import PromptDeploymentView from "./promptDeploymentView";
export type SettingsSidebarSection = "parameters" | "deployments";
export function PromptDeploymentsAccordionItem({ activeSection }: { activeSection: SettingsSidebarSection | undefined }) {
const { selectedPromptId } = usePromptContext();
if (!selectedPromptId) {
return null;
}
const deploymentsOpen = activeSection === "deployments";
return (
<AccordionItem
value="deployments"
className={cn(
"border-border/60 flex min-h-0 flex-col border-b-0 border-t pt-1",
deploymentsOpen ? "min-h-0 grow overflow-hidden" : "shrink-0 grow-0",
)}
>
<AccordionTrigger
data-testid="prompt-deployments-trigger"
className="text-muted-foreground w-full min-w-0 shrink-0 py-3 pr-1 text-xs font-medium uppercase hover:no-underline [&[data-state=open]>svg]:rotate-180"
>
<span className="min-w-0 flex-1 text-left font-semibold">Deployments</span>
</AccordionTrigger>
<AccordionContent
containerClassName="data-[state=open]:flex data-[state=open]:min-h-0 data-[state=open]:flex-1 data-[state=open]:flex-col"
className="min-h-0 flex-1 overflow-y-auto pt-0 pb-2"
>
<PromptDeploymentView omitTitle />
</AccordionContent>
</AccordionItem>
);
}

View File

@@ -0,0 +1,16 @@
import { UserRoundCheck } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function RBACView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<UserRoundCheck className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock roles and permissions for better security"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/advanced-governance"
/>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { BookUser } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function SCIMView() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<BookUser className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock SCIM based access management for user provisioning"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/advanced-governance"
/>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { Building2 } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export function BusinessUnitsView() {
return (
<div className="w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
testIdPrefix="business-units-governance"
icon={<Building2 className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock business units & advanced governance"
description="Manage users, business units with our enterprise-grade governance. This feature is part of the Bifrost enterprise license."
readmeLink="https://docs.getbifrost.ai/enterprise/advanced-governance"
/>
</div>
);
}

View File

@@ -0,0 +1,105 @@
import TeamsTable from "@/app/workspace/governance/views/teamsTable";
import FullPageLoader from "@/components/fullPageLoader";
import { useDebouncedValue } from "@/hooks/useDebounce";
import { getErrorMessage, useGetCustomersQuery, useGetTeamsQuery, useGetVirtualKeysQuery } from "@/lib/store";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
const POLLING_INTERVAL = 5000;
const PAGE_SIZE = 25;
export function TeamsView() {
const hasVirtualKeysAccess = useRbac(RbacResource.VirtualKeys, RbacOperation.View);
const hasCustomersAccess = useRbac(RbacResource.Customers, RbacOperation.View);
const hasTeamsAccess = useRbac(RbacResource.Teams, RbacOperation.View);
const shownErrorsRef = useRef(new Set<string>());
const [search, setSearch] = useState("");
const [offset, setOffset] = useState(0);
const debouncedSearch = useDebouncedValue(search, 300);
useEffect(() => {
setOffset(0);
}, [debouncedSearch]);
const {
data: virtualKeysData,
error: vkError,
isLoading: vkLoading,
} = useGetVirtualKeysQuery(undefined, {
skip: !hasVirtualKeysAccess,
pollingInterval: POLLING_INTERVAL,
});
const {
data: customersData,
error: customersError,
isLoading: customersLoading,
} = useGetCustomersQuery(undefined, {
skip: !hasCustomersAccess,
pollingInterval: POLLING_INTERVAL,
});
const {
data: teamsData,
error: teamsError,
isLoading: teamsLoading,
} = useGetTeamsQuery(
{
limit: PAGE_SIZE,
offset,
search: debouncedSearch || undefined,
},
{
skip: !hasTeamsAccess,
pollingInterval: POLLING_INTERVAL,
},
);
const teamsTotal = teamsData?.total_count ?? 0;
// Snap offset back when total shrinks past current page (e.g. delete last item on last page)
useEffect(() => {
if (!teamsData || offset < teamsTotal) return;
setOffset(teamsTotal === 0 ? 0 : Math.floor((teamsTotal - 1) / PAGE_SIZE) * PAGE_SIZE);
}, [teamsTotal, offset]);
const isLoading = vkLoading || customersLoading || teamsLoading;
useEffect(() => {
if (!vkError && !customersError && !teamsError) {
shownErrorsRef.current.clear();
return;
}
const errorKey = `${!!vkError}-${!!customersError}-${!!teamsError}`;
if (shownErrorsRef.current.has(errorKey)) return;
shownErrorsRef.current.add(errorKey);
if (vkError && customersError && teamsError) {
toast.error("Failed to load governance data.");
} else {
if (vkError) toast.error(`Failed to load virtual keys: ${getErrorMessage(vkError)}`);
if (customersError) toast.error(`Failed to load customers: ${getErrorMessage(customersError)}`);
if (teamsError) toast.error(`Failed to load teams: ${getErrorMessage(teamsError)}`);
}
}, [vkError, customersError, teamsError]);
if (isLoading) {
return <FullPageLoader />;
}
return (
<div className="mx-auto w-full max-w-7xl">
<TeamsTable
teams={teamsData?.teams || []}
totalCount={teamsData?.total_count || 0}
customers={customersData?.customers || []}
virtualKeys={virtualKeysData?.virtual_keys || []}
search={search}
debouncedSearch={debouncedSearch}
onSearchChange={setSearch}
offset={offset}
limit={PAGE_SIZE}
onOffsetChange={setOffset}
/>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { Users } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function UsersView() {
return (
<div className="w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<Users className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock users & user governance"
description="Manage users, set per-user budgets and rate limits, and control access with enterprise-grade governance. This feature is part of the Bifrost enterprise license."
readmeLink="https://docs.getbifrost.ai/enterprise/advanced-governance"
/>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { Users } from "lucide-react";
import ContactUsView from "../views/contactUsView";
export default function UserRankingsTab() {
return (
<div className="h-full w-full">
<ContactUsView
className="mx-auto min-h-[80vh]"
icon={<Users className="h-[5.5rem] w-[5.5rem]" strokeWidth={1} />}
title="Unlock user rankings for better visibility"
description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you."
readmeLink="https://docs.getbifrost.ai/enterprise/user-rankings"
testIdPrefix="user-rankings"
/>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ArrowUpRight } from "lucide-react";
interface Props {
className?: string;
icon: React.ReactNode;
title: string;
description: string;
readmeLink: string;
align?: "middle" | "top";
testIdPrefix?: string;
}
export default function ContactUsView({ icon, title, description, className, readmeLink, align = "middle", testIdPrefix }: Props) {
return (
<div className={cn("flex flex-col items-center gap-4 text-center", align === "middle" ? "justify-center" : "justify-start", className)}>
<div className="text-muted-foreground">{icon}</div>
<div className="flex flex-col gap-1">
<h1 className="text-muted-foreground text-xl font-medium">{title}</h1>
<div className="text-muted-foreground mt-2 max-w-[600px] text-sm font-normal">{description}</div>
<div className="mx-auto flex flex-row items-center gap-2">
<Button
variant="outline"
aria-label="Read more about this feature (opens in new tab)"
className="mx-auto mt-6"
data-testid={testIdPrefix ? `${testIdPrefix}-read-more` : undefined}
onClick={() => {
window.open(`${readmeLink}?utm_source=bfd`, "_blank", "noopener,noreferrer");
}}
>
Read more <ArrowUpRight className="text-muted-foreground h-3 w-3" />
</Button>
<Button
className="mx-auto mt-6"
aria-label="Book a demo (opens Calendly in new tab)"
data-testid={testIdPrefix ? `${testIdPrefix}-book-demo` : undefined}
onClick={() => {
window.open("https://calendly.com/maximai/bifrost-demo?utm_source=bfd_ent", "_blank", "noopener,noreferrer");
}}
>
Book a demo
</Button>
</div>
</div>
</div>
);
}