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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Router } from "lucide-react";
import ContactUsView from "../views/contactUsView";

export default function PromptDeploymentView() {
export default function PromptDeploymentView(_props?: { omitTitle?: boolean }) {
Comment thread
roroghost17 marked this conversation as resolved.
return (
<div className="w-full">
<ContactUsView
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use client";

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">
<span className="min-w-0 flex-1 text-left">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 pb-2 pt-0"
>
<PromptDeploymentView omitTitle />
</AccordionContent>
</AccordionItem>
);
}
58 changes: 38 additions & 20 deletions ui/app/workspace/routing-rules/views/routingRuleSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,54 @@
* Create/Edit form for routing rules
*/

import { useState, useEffect, useCallback } from "react";
import { useForm } from "react-hook-form";
import { RuleGroupType } from "react-querybuilder";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
"use client";

import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { ModelMultiselect } from "@/components/ui/modelMultiselect";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Switch } from "@/components/ui/switch";
import { ModelMultiselect } from "@/components/ui/modelMultiselect";
import { X, Save, Plus, Trash2 } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
import { ProviderIconType, RenderProviderIcon } from "@/lib/constants/icons";
import { getProviderLabel } from "@/lib/constants/logs";
import { getErrorMessage } from "@/lib/store";
import {
useGetCustomersQuery,
useGetTeamsQuery,
useGetVirtualKeysQuery,
} from "@/lib/store/apis/governanceApi";
import { useGetAllKeysQuery, useGetProvidersQuery } from "@/lib/store/apis/providersApi";
import {
useCreateRoutingRuleMutation,
useGetRoutingRulesQuery,
useUpdateRoutingRuleMutation,
} from "@/lib/store/apis/routingRulesApi";
import {
RoutingRule,
RoutingRuleFormData,
RoutingTargetFormData,
DEFAULT_ROUTING_RULE_FORM_DATA,
DEFAULT_ROUTING_TARGET,
ROUTING_RULE_SCOPES,
RoutingRule,
RoutingRuleFormData,
RoutingTargetFormData,
} from "@/lib/types/routingRules";
import { useCreateRoutingRuleMutation, useUpdateRoutingRuleMutation, useGetRoutingRulesQuery } from "@/lib/store/apis/routingRulesApi";
import { useGetVirtualKeysQuery, useGetTeamsQuery, useGetCustomersQuery } from "@/lib/store/apis/governanceApi";
import { useGetProvidersQuery, useGetAllKeysQuery } from "@/lib/store/apis/providersApi";
import {
validateRateLimitAndBudgetRules,
validateRoutingRules
} from "@/lib/utils/celConverterRouting";
import { Plus, Save, Trash2, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { RuleGroupType } from "react-querybuilder";
import { toast } from "sonner";
import { Suspense, lazy } from "react";
import { ProviderIconType, RenderProviderIcon } from "@/lib/constants/icons";
import { getProviderLabel } from "@/lib/constants/logs";
import { Separator } from "@/components/ui/separator";
import { getErrorMessage } from "@/lib/store";
import { validateRoutingRules, validateRateLimitAndBudgetRules } from "@/lib/utils/celConverterRouting";

interface RoutingRuleDialogProps {
open: boolean;
Expand Down
165 changes: 103 additions & 62 deletions ui/components/prompts/fragments/settingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { ComboboxSelect } from "@/components/ui/combobox";
import ModelParameters from "@/components/ui/custom/modelParameters";
import { Label } from "@/components/ui/label";
import { ModelMultiselect } from "@/components/ui/modelMultiselect";
import { ScrollArea } from "@/components/ui/scrollArea";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { getProviderLabel } from "@/lib/constants/logs";
import { useGetVirtualKeysQuery } from "@/lib/store";
import { useGetAllKeysQuery, useGetProvidersQuery } from "@/lib/store/apis/providersApi";
import { ModelProviderName } from "@/lib/types/config";
import { ModelParams } from "@/lib/types/prompts";
import PromptDeploymentView from "@enterprise/components/prompt-deployments/promptDeploymentView";
import { useCallback, useMemo } from "react";
import { cn } from "@/lib/utils";
import { PromptDeploymentsAccordionItem } from "@enterprise/components/prompt-deployments/promptDeploymentsAccordionItem";
import { useCallback, useMemo, useState } from "react";
import { ApiKeySelectorView } from "../components/apiKeySelectorView";
import { VariablesTableView } from "../components/variablesTableView";
import { usePromptContext } from "../context";
Expand All @@ -26,6 +27,9 @@ export function SettingsPanel() {
setModelParams: onModelParamsChange,
apiKeyId,
setApiKeyId,
variables,
setVariables,
selectedPromptId,
} = usePromptContext();

const onProviderChange = useCallback(
Expand Down Expand Up @@ -113,6 +117,11 @@ export function SettingsPanel() {
[onModelParamsChange],
);

const hasModel = Boolean(model);

type SettingsSection = "parameters" | "deployments";
const [openSection, setOpenSection] = useState<SettingsSection | undefined>("parameters");
Comment thread
roroghost17 marked this conversation as resolved.

if (isInitialLoading) {
return (
<div className="flex h-full flex-col">
Expand All @@ -131,66 +140,98 @@ export function SettingsPanel() {
}

return (
<div className="flex h-full flex-col">
<ScrollArea className="grow overflow-y-auto" viewportClassName="no-table">
<div className="space-y-6 p-4">
<div className="flex flex-col gap-2" data-testid="settings-provider">
<Label className="text-muted-foreground text-xs font-medium uppercase">Provider</Label>
<ComboboxSelect
options={providerOptions}
value={provider}
onValueChange={(v) => v && onProviderChange(v)}
placeholder="Select provider"
hideClear
/>
</div>

<div className="flex flex-col gap-2" data-testid="settings-model">
<Label className="text-muted-foreground text-xs font-medium uppercase">Model</Label>
<ModelMultiselect
provider={provider}
keys={filterKeys && filterKeys.length > 0 ? filterKeys : undefined}
vks={filterVks}
value={model}
onChange={(v) => onModelChange(v)}
isSingleSelect
unfiltered
placeholder={!provider ? "Select a provider first" : "Select model"}
disabled={!provider}
/>
</div>

{(providerKeys.length > 0 || providerVirtualKeys.length > 0) && !!provider && (
<ApiKeySelectorView
providerKeys={providerKeys}
virtualKeys={providerVirtualKeys}
value={apiKeyId}
onValueChange={(v) => onApiKeyIdChange(v ?? "__auto__")}
disabled={!provider}
/>
)}

{Object.keys(variables).length > 0 && (
<>
<Separator />
<VariablesTableView variables={variables} onChange={setVariables} />
</>
)}

{model && (
<>
<Separator />

<div className="flex flex-col gap-4">
<Label className="text-muted-foreground text-xs font-medium uppercase">Model Parameters</Label>
<ModelParameters model={model} config={modelParams} onChange={handleModelParamsChange} hideFields={["promptTools"]} />
<div className="flex h-full min-h-0 flex-col">
<div className="flex min-h-0 flex-1 flex-col px-4 pb-4 pt-2">
<Accordion
type="single"
collapsible
value={openSection ?? ""}
onValueChange={(v) => {
if (v === "parameters" || v === "deployments") {
setOpenSection(v);
} else {
setOpenSection(undefined);
}
}}
className="flex min-h-0 flex-1 flex-col"
Comment on lines 122 to +156
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale openSection state when a prompt is deselected

When the user has the "Deployments" section active (openSection === "deployments") and then deselects the current prompt, selectedPromptId becomes falsy and PromptDeploymentsAccordionItem unmounts — but openSection stays "deployments". Because the AccordionItem for "Configuration" computes its className using openSection:

openSection === "parameters" ? "flex-1" : "shrink-0 overflow-hidden"

…it gets "shrink-0 overflow-hidden" even though nothing is actually open, leaving the panel showing only a shrunken collapsed trigger bar. The user has to manually click "Configuration" to restore a usable layout.

Consider resetting openSection back to "parameters" when selectedPromptId is cleared:

useEffect(() => {
  if (!selectedPromptId && openSection === "deployments") {
    setOpenSection("parameters");
  }
}, [selectedPromptId, openSection]);

>
<AccordionItem
value="parameters"
className={cn(
"flex min-h-0 flex-col border-b-0",
openSection === "parameters" ? "flex-1" : "shrink-0 overflow-hidden",
)}
>
<AccordionTrigger data-testid="prompts-configuration-trigger" className="text-muted-foreground shrink-0 py-3 pr-1 text-xs font-medium uppercase hover:no-underline">
<span className="min-w-0 flex-1 text-left font-semibold">Configuration</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 pb-2 pt-0"
>
<div className="space-y-6">
<div className="flex flex-col gap-2" data-testid="settings-provider">
<Label className="text-muted-foreground text-xs font-medium uppercase">Provider</Label>
<ComboboxSelect
options={providerOptions}
value={provider}
onValueChange={(v) => v && onProviderChange(v)}
placeholder="Select provider"
hideClear
/>
</div>

<div className="flex flex-col gap-2" data-testid="settings-model">
<Label className="text-muted-foreground text-xs font-medium uppercase">Model</Label>
<ModelMultiselect
provider={provider}
keys={filterKeys && filterKeys.length > 0 ? filterKeys : undefined}
vks={filterVks}
value={model}
onChange={(v) => onModelChange(v)}
isSingleSelect
placeholder={!provider ? "Select a provider first" : "Select model"}
disabled={!provider}
unfiltered={true}
/>
</div>

{(providerKeys.length > 0 || providerVirtualKeys.length > 0) && !!provider && (
<ApiKeySelectorView
providerKeys={providerKeys}
virtualKeys={providerVirtualKeys}
value={apiKeyId}
onValueChange={(v) => onApiKeyIdChange(v ?? "__auto__")}
disabled={!provider}
/>
)}

{Object.keys(variables).length > 0 && (
<>
<Separator />
<VariablesTableView variables={variables} onChange={setVariables} />
</>
)}

{hasModel && (
<>
<Separator />
<div className="flex flex-col gap-4">
<ModelParameters
model={model}
config={modelParams}
onChange={handleModelParamsChange}
hideFields={["promptTools"]}
/>
</div>
</>
)}
</div>
</>
)}

<PromptDeploymentView />
</div>
</ScrollArea>
</AccordionContent>
</AccordionItem>
{selectedPromptId && <PromptDeploymentsAccordionItem activeSection={openSection} />}
Comment thread
roroghost17 marked this conversation as resolved.
</Accordion>
</div>
</div>
);
}
23 changes: 17 additions & 6 deletions ui/components/ui/accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,43 @@ function AccordionItem({ className, ...props }: React.ComponentProps<typeof Acco

function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Header className="flex w-full min-w-0">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-sm py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
"focus-visible:border-ring focus-visible:ring-ring/50 flex w-full min-w-0 flex-1 items-center justify-between gap-2 rounded-sm py-4 text-left text-sm font-medium transition-colors outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none my-auto size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
Comment thread
roroghost17 marked this conversation as resolved.
}

function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
function AccordionContent({
className,
containerClassName,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content> & {
/** Classes merged onto the Radix content wrapper (outer). `className` still targets the inner div. */
containerClassName?: string;
}) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
className={cn(
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm",
containerClassName,
)}
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
);
}

export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
Loading