various ui improvements

This commit is contained in:
miloschwartz
2026-08-13 14:54:09 -04:00
parent db716f781e
commit d9a9ae14fd
6 changed files with 102 additions and 83 deletions
+8 -8
View File
@@ -864,9 +864,9 @@
"policyAuthMethodOff": "Off", "policyAuthMethodOff": "Off",
"policyAuthSsoTitle": "Platform SSO", "policyAuthSsoTitle": "Platform SSO",
"policyAuthSsoDescription": "Require sign-in through your organization's identity provider", "policyAuthSsoDescription": "Require sign-in through your organization's identity provider",
"policyAuthInferenceSsoDescription": "Selected users and roles can authenticate to the gateway using their identity API key", "policyAuthInferenceSsoDescription": "Selected users and roles can authenticate to the gateway using their identity API key.",
"policyAuthInferenceIdentityKeyHelp": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users can retrieve their key by signing in with their identity provider at <resourceLink></resourceLink>, where it will be shown after login.",
"policyAuthInferenceIdentityKeyHelpNoUrl": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users can retrieve their key by signing in with their identity provider at this resource's URL, where it will be shown after login.", "policyAuthInferenceIdentityKeyHelpNoUrl": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users can retrieve their key by signing in with their identity provider at this resource's URL, where it will be shown after login.",
"policyAuthInferenceIdentityKeyHelp": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users can retrieve their key by signing in with their identity provider at <resourceLink></resourceLink>, where it will be shown after login.",
"policyAuthSsoSummary": "{idp} · {users} users, {roles} roles", "policyAuthSsoSummary": "{idp} · {users} users, {roles} roles",
"policyAuthSsoDefaultIdp": "Default provider", "policyAuthSsoDefaultIdp": "Default provider",
"policyAuthAddDefaultIdentityProvider": "Add Default Identity Provider", "policyAuthAddDefaultIdentityProvider": "Add Default Identity Provider",
@@ -1821,7 +1821,7 @@
"aiProviderBudgetUnitUsd": "USD", "aiProviderBudgetUnitUsd": "USD",
"aiProviderBudgetUnitTokens": "Tokens", "aiProviderBudgetUnitTokens": "Tokens",
"aiProviderEnabled": "Enabled", "aiProviderEnabled": "Enabled",
"aiProviderEnabledDescription": "Disable to stop using this provider without deleting it", "aiProviderEnabledDescription": "Fully disable this provider across all resources",
"aiProviderErrorCreate": "Failed to create AI provider", "aiProviderErrorCreate": "Failed to create AI provider",
"aiProviderErrorUpdate": "Failed to update AI provider", "aiProviderErrorUpdate": "Failed to update AI provider",
"aiProviderErrorDelete": "Failed to delete AI provider", "aiProviderErrorDelete": "Failed to delete AI provider",
@@ -1868,11 +1868,11 @@
"aiProviderModelsAllow": "Allow List", "aiProviderModelsAllow": "Allow List",
"aiProviderModelsAllowDescription": "Models that may be used through this provider. Empty means deny all.", "aiProviderModelsAllowDescription": "Models that may be used through this provider. Empty means deny all.",
"aiProviderModelsAllowPlaceholder": "Enter model key", "aiProviderModelsAllowPlaceholder": "Enter model key",
"aiProviderModelsAllowEmpty": "No models allowed. All requests will be denied.", "aiProviderModelsAllowEmpty": "No models added yet.",
"aiProviderModelsBlock": "Block List", "aiProviderModelsBlock": "Block List",
"aiProviderModelsBlockDescription": "Models to deny even if they match an allow entry.", "aiProviderModelsBlockDescription": "Models to deny even if they match an allow entry.",
"aiProviderModelsBlockPlaceholder": "Enter model key", "aiProviderModelsBlockPlaceholder": "Enter model key",
"aiProviderModelsBlockEmpty": "No blocked models.", "aiProviderModelsBlockEmpty": "No models added yet.",
"aiProviderModelsAdd": "Add Models", "aiProviderModelsAdd": "Add Models",
"aiProviderModelsClearAll": "Clear All", "aiProviderModelsClearAll": "Clear All",
"aiProviderModelsAddCustom": "Add \"{key}\"", "aiProviderModelsAddCustom": "Add \"{key}\"",
@@ -1938,7 +1938,7 @@
"aiResourceProviderAllowModelsEmpty": "No models found", "aiResourceProviderAllowModelsEmpty": "No models found",
"aiResourceProviderAllowModelsHelp": "Only models from this provider's allow list can be selected.", "aiResourceProviderAllowModelsHelp": "Only models from this provider's allow list can be selected.",
"aiResourceAliasRequired": "Alias is required for inference resources", "aiResourceAliasRequired": "Alias is required for inference resources",
"aiResourceDomainConfiguration": "Domain configuration", "aiResourceDomainConfiguration": "Domain Configuration",
"aiResourceDomainConfigurationDescription": "Choose the domain clients will use to reach this inference resource.", "aiResourceDomainConfigurationDescription": "Choose the domain clients will use to reach this inference resource.",
"aiUsageAnalyticsTitle": "AI Usage Analytics", "aiUsageAnalyticsTitle": "AI Usage Analytics",
"aiUsageAnalyticsDescription": "Analyze AI gateway cost, token usage, and activity across providers, resources, roles, and users", "aiUsageAnalyticsDescription": "Analyze AI gateway cost, token usage, and activity across providers, resources, roles, and users",
@@ -3720,9 +3720,9 @@
"internalResourceFormMultiSiteRoutingHelp": "Selecting multiple sites enables resilient routing and failover for high availability.", "internalResourceFormMultiSiteRoutingHelp": "Selecting multiple sites enables resilient routing and failover for high availability.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Learn more", "internalResourceFormMultiSiteRoutingHelpLearnMore": "Learn more",
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.", "editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
"createInternalResourceDialogHttpConfiguration": "HTTP configuration", "createInternalResourceDialogHttpConfiguration": "Domain Configuration",
"createInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.", "createInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
"editInternalResourceDialogHttpConfiguration": "HTTP configuration", "editInternalResourceDialogHttpConfiguration": "Domain Configuration",
"editInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.", "editInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
"editInternalResourceDialogTcp": "TCP", "editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP", "editInternalResourceDialogUdp": "UDP",
@@ -271,14 +271,14 @@ export default function CreateAiProviderPage() {
value value
) )
); );
const currentName =
form.getValues(
"name"
);
if ( if (
value !== value !==
"custom" "custom"
) { ) {
const currentName =
form.getValues(
"name"
);
const previousLabel = const previousLabel =
t( t(
aiProviderTypeLabelMap[ aiProviderTypeLabelMap[
@@ -305,6 +305,30 @@ export default function CreateAiProviderPage() {
); );
targetsRef.current = targetsRef.current =
[]; [];
} else {
const isDefaultName =
Object.entries(
aiProviderTypeLabelMap
).some(
([
type,
key
]) =>
type !==
"custom" &&
currentName ===
t(
key
)
);
if (
isDefaultName
) {
form.setValue(
"name",
""
);
}
} }
}} }}
/> />
@@ -86,6 +86,13 @@ export type ProxyResourceTargetsFormHandle = {
save: (options?: { silent?: boolean }) => Promise<boolean>; save: (options?: { silent?: boolean }) => Promise<boolean>;
}; };
const DEFAULT_ALLOWED_METHODS: ("http" | "https" | "h2c")[] = [
"http",
"https",
"h2c"
];
const EMPTY_TARGETS: LocalTarget[] = [];
type ProxyResourceTargetsFormProps = { type ProxyResourceTargetsFormProps = {
orgId: string; orgId: string;
isHttp: boolean; isHttp: boolean;
@@ -115,12 +122,12 @@ export const ProxyResourceTargetsForm = forwardRef<
{ {
orgId, orgId,
isHttp, isHttp,
initialTargets = [], initialTargets = EMPTY_TARGETS,
resource, resource,
providerId, providerId,
updateResource, updateResource,
onChange, onChange,
allowedMethods = ["http", "https", "h2c"], allowedMethods = DEFAULT_ALLOWED_METHODS,
emptyMessage, emptyMessage,
embedded = false, embedded = false,
hideSaveButton = false, hideSaveButton = false,
@@ -266,7 +273,7 @@ export const ProxyResourceTargetsForm = forwardRef<
); );
}); });
}, },
[sites] []
); );
const openHealthCheckDialog = useCallback((target: LocalTarget) => { const openHealthCheckDialog = useCallback((target: LocalTarget) => {
@@ -616,7 +623,6 @@ export const ProxyResourceTargetsForm = forwardRef<
}, [ }, [
isAdvancedMode, isAdvancedMode,
isHttp, isHttp,
sites,
updateTarget, updateTarget,
getDockerStateForSite, getDockerStateForSite,
refreshContainersForSite, refreshContainersForSite,
+54 -66
View File
@@ -350,28 +350,24 @@ export function AiProviderModelListEditor({
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div> {items.length === 0 ? (
<div className="relative"> <p className="text-sm text-muted-foreground">{emptyMessage}</p>
<div ) : (
ref={gridRef} <div>
className={cn( <div className="relative">
"grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3", <div
isCollapsed && "overflow-hidden" ref={gridRef}
)} className={cn(
style={ "grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3",
isCollapsed && clipHeight != null isCollapsed && "overflow-hidden"
? { maxHeight: clipHeight } )}
: undefined style={
} isCollapsed && clipHeight != null
> ? { maxHeight: clipHeight }
{items.length === 0 ? ( : undefined
<div className="flex min-w-0 items-center justify-center rounded-md border border-dashed border-input px-2.5 py-2"> }
<span className="text-xs text-muted-foreground"> >
{emptyMessage} {items.map((item) => (
</span>
</div>
) : (
items.map((item) => (
<ModelCard <ModelCard
key={item.clientId} key={item.clientId}
item={item} item={item}
@@ -385,42 +381,42 @@ export function AiProviderModelListEditor({
} }
onRemove={() => removeModel(item.clientId)} onRemove={() => removeModel(item.clientId)}
/> />
)) ))}
)} </div>
{isCollapsed ? (
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-14 bg-gradient-to-t from-card from-25% via-card/80 to-transparent" />
) : null}
</div> </div>
{isCollapsed ? ( {isCollapsed ? (
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-14 bg-gradient-to-t from-card from-25% via-card/80 to-transparent" /> <div className="relative z-10 flex justify-center pt-2">
<Button
type="button"
variant="text"
size="sm"
className="bg-card px-2 text-muted-foreground hover:text-foreground"
onClick={() => setListExpanded(true)}
>
{t("aiProviderModelsViewMore", {
count: items.length - collapsedLimit
})}
</Button>
</div>
) : null}
{hasOverflow && listExpanded ? (
<div className="flex justify-center pt-1">
<Button
type="button"
variant="text"
size="sm"
className="text-muted-foreground hover:text-foreground"
onClick={() => setListExpanded(false)}
>
{t("aiProviderModelsViewLess")}
</Button>
</div>
) : null} ) : null}
</div> </div>
{isCollapsed ? ( )}
<div className="relative z-10 flex justify-center pt-2">
<Button
type="button"
variant="text"
size="sm"
className="bg-card px-2 text-muted-foreground hover:text-foreground"
onClick={() => setListExpanded(true)}
>
{t("aiProviderModelsViewMore", {
count: items.length - collapsedLimit
})}
</Button>
</div>
) : null}
{hasOverflow && listExpanded ? (
<div className="flex justify-center pt-1">
<Button
type="button"
variant="text"
size="sm"
className="text-muted-foreground hover:text-foreground"
onClick={() => setListExpanded(false)}
>
{t("aiProviderModelsViewLess")}
</Button>
</div>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Popover <Popover
@@ -828,9 +824,7 @@ function EditModelCredenza({
defaultValues: { modelKey: item.modelKey } defaultValues: { modelKey: item.modelKey }
}); });
const [pendingBudgetRows, setPendingBudgetRows] = useState<BudgetRow[]>( const [pendingBudgetRows, setPendingBudgetRows] = useState<BudgetRow[]>([]);
[]
);
const [attemptedBudgetsSave, setAttemptedBudgetsSave] = useState(false); const [attemptedBudgetsSave, setAttemptedBudgetsSave] = useState(false);
const [savingBudgets, setSavingBudgets] = useState(false); const [savingBudgets, setSavingBudgets] = useState(false);
@@ -929,9 +923,7 @@ function EditModelCredenza({
items={[ items={[
{ title: t("general"), href: "#" }, { title: t("general"), href: "#" },
{ {
title: t( title: t("aiProviderModelsBudgetTab"),
"aiProviderModelsBudgetTab"
),
href: "#" href: "#"
} }
]} ]}
@@ -970,9 +962,7 @@ function EditModelCredenza({
</p> </p>
<BudgetRowsFields <BudgetRowsFields
rows={pendingBudgetRows} rows={pendingBudgetRows}
onChange={ onChange={setPendingBudgetRows}
setPendingBudgetRows
}
disabled={ disabled={
budgetsQuery.isLoading || budgetsQuery.isLoading ||
savingBudgets savingBudgets
@@ -984,9 +974,7 @@ function EditModelCredenza({
</> </>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t( {t("aiProviderModelsBudgetUnsaved")}
"aiProviderModelsBudgetUnsaved"
)}
</p> </p>
)} )}
</div> </div>
@@ -573,7 +573,6 @@ export function PolicyAuthStackSectionEdit({
className="text-primary hover:underline" className="text-primary hover:underline"
> >
{inferenceResourceUrl} {inferenceResourceUrl}
<ExternalLink className="ml-1 inline size-3.5 shrink-0 align-text-bottom" />
</a> </a>
) )
} }
@@ -142,6 +142,8 @@ export function ResourceTargetAddressItem({
isHttp, isHttp,
allowedMethods = ["http", "https", "h2c"] allowedMethods = ["http", "https", "h2c"]
}: ResourceTargetAddressItemProps) { }: ResourceTargetAddressItemProps) {
"use no memo";
return ( return (
<div className="flex items-center w-full" key={proxyTarget.targetId}> <div className="flex items-center w-full" key={proxyTarget.targetId}>
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md"> <div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">