feat(providers): hide disabled types from create menu + guard ?type= URL

A user could create a provider of a type whose API key isn't configured: it
saved fine and showed in settings, but was dead for flow creation with no
signal (and used to break the whole providers query — see 08a24c9). Two layers:
- the "Create provider" menu now only offers types whose key is set
  (settingsProviders.enabled, already fetched by the page), and
- the create form bounces a hand-typed ?type= that is unknown or disabled to
  the list, closing the direct-URL bypass of the filtered menu.

Frontend-only. Live-verified on the docker stand: minimax/custom drop from the
menu (11 -> 9); ?type=minimax and ?type=garbage123 redirect; ?type=anthropic
still opens the form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-06-27 23:07:53 +07:00
co-authored by Claude Opus 4.8
parent 4664cf8f53
commit b6d10362cf
2 changed files with 34 additions and 14 deletions
@@ -1065,8 +1065,10 @@ const extractAgentTypes = (agents: unknown): null | string[] => {
return types.length > 0 ? types : null;
};
interface DeleteProviderDialogProps
extends Pick<ComponentProps<typeof ConfirmationDialog>, 'handleConfirm' | 'handleOpenChange' | 'isOpen'> {
interface DeleteProviderDialogProps extends Pick<
ComponentProps<typeof ConfirmationDialog>,
'handleConfirm' | 'handleOpenChange' | 'isOpen'
> {
control: Control<FormInput>;
}
@@ -1258,6 +1260,16 @@ function SettingsProvider() {
const queryType = formQueryParams.type ?? undefined;
const queryId = formQueryParams.id;
// A hand-typed ?type= URL bypasses the create menu's enabled-only filter; an
// unknown or disabled type would otherwise create a dead provider or dump a raw
// zod error on submit. Bounce it to the list. (Clone-by-id, below, is exempt.)
if (!queryId && queryType && !providers.enabled[queryType as keyof typeof providers.enabled]) {
toast.error(`Provider type "${queryType}" is not available`);
navigate(routes.settings.providers, { replace: true });
return;
}
if (queryId && data?.settingsProviders?.userDefined) {
const sourceProvider = data.settingsProviders.userDefined.find((p: Provider) => p.id == queryId);
@@ -452,6 +452,9 @@ function SettingsProviders() {
function SettingsProvidersHeader() {
const navigate = useNavigate();
// Cached: the list above already fetched this query, so the read is local.
const { data } = useQuery(SettingsProvidersDocument);
const enabled = data?.settingsProviders?.enabled;
const handleProviderCreate = (providerType: string) => {
navigate(routes.settings.newProvider({ type: providerType }));
@@ -486,19 +489,24 @@ function SettingsProvidersHeader() {
width: 'var(--radix-dropdown-menu-trigger-width)',
}}
>
{providerTypes.map(({ label, type }) => {
const Icon = providerIcons[type]?.icon;
{/* Only offer types whose API key is configured — creating a provider of a
disabled type yields one that's unusable for flows (the create form guards
the same against a hand-typed ?type=). */}
{providerTypes
.filter(({ type }) => enabled?.[type as keyof typeof enabled])
.map(({ label, type }) => {
const Icon = providerIcons[type]?.icon;
return (
<DropdownMenuItem
key={type}
onClick={() => handleProviderCreate(type)}
>
{Icon && <Icon className="size-4" />}
{label}
</DropdownMenuItem>
);
})}
return (
<DropdownMenuItem
key={type}
onClick={() => handleProviderCreate(type)}
>
{Icon && <Icon className="size-4" />}
{label}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>