Break settings out from Clerk org component (#54)

This commit is contained in:
Chris Estreich 2025-05-30 15:18:39 -07:00 committed by GitHub
parent 357bdefb8f
commit d5db42ddf3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 505 additions and 568 deletions

View file

@ -1,162 +0,0 @@
'use client';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Cloud } from 'lucide-react';
import {
type OrganizationSettings,
type OrganizationCloudSettings,
} from '@/types';
import {
getOrganizationSettings,
updateOrganization,
} from '@/actions/organizationSettings';
import {
Button,
Checkbox,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from '@/components/ui';
type CloudSettingsFormValues = {
recordTaskMessages: boolean;
};
type CloudSettingsFormProps = {
orgSettings: OrganizationSettings;
queryClient: ReturnType<typeof useQueryClient>;
};
const CloudSettingsForm = ({
orgSettings,
queryClient,
}: CloudSettingsFormProps) => {
const t = useTranslations('CloudSettings');
const [isSaving, setIsSaving] = useState(false);
const form = useForm<CloudSettingsFormValues>({
defaultValues: {
recordTaskMessages:
orgSettings.cloudSettings?.recordTaskMessages ?? false,
},
});
const onSubmit = async (data: CloudSettingsFormValues) => {
setIsSaving(true);
try {
const cloudSettings: OrganizationCloudSettings = {
recordTaskMessages: data.recordTaskMessages,
};
const result = await updateOrganization({ cloudSettings });
if (result.success) {
queryClient.invalidateQueries({
queryKey: ['getOrganizationSettings'],
});
toast.success('Cloud settings saved successfully');
} else {
throw new Error(result.error || 'An unexpected error occurred.');
}
} catch (error) {
console.error('Failed to update cloud settings:', error);
toast.error('Failed to save cloud settings. Please try again.');
} finally {
setIsSaving(false);
}
};
return (
<>
<div className="cl-header 🔒️ cl-internal-qo3qk7">
<div className="cl-internal-1pr5xvn">
<h1 className="cl-headerTitle 🔒️ cl-internal-190cjq9">
{t('cloud_section_title')}
</h1>
</div>
</div>
<p className="mb-6 text-sm text-muted-foreground">
{t('cloud_section_description')}
</p>
<div className="space-y-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4 rounded-lg p-4">
<div className="flex items-center gap-2">
<Cloud className="size-5" />
<h2 className="text-lg font-medium">Task Recording</h2>
</div>
<div className="mt-4 space-y-4">
<FormField
control={form.control}
name="recordTaskMessages"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
disabled={isSaving}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>{t('record_task_messages')}</FormLabel>
<FormDescription>
{t('record_task_messages_description')}
</FormDescription>
</div>
</FormItem>
)}
/>
</div>
</div>
<div className="flex justify-end space-x-4 border-t pt-4">
<Button type="submit" disabled={isSaving}>
{isSaving ? (
<>
<span className="mr-2">Saving...</span>
<span className="animate-spin"></span>
</>
) : (
'Save Changes'
)}
</Button>
</div>
</form>
</Form>
</div>
</>
);
};
export const CloudSettings = () => {
const queryClient = useQueryClient();
const { data: orgSettings } = useQuery({
queryKey: ['getOrganizationSettings'],
queryFn: getOrganizationSettings,
});
if (!orgSettings) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin text-2xl"></div>
<span className="ml-2">Loading settings...</span>
</div>
);
}
return (
<CloudSettingsForm orgSettings={orgSettings} queryClient={queryClient} />
);
};

View file

@ -1,37 +0,0 @@
'use client';
import { useTranslations } from 'next-intl';
import { OrganizationProfile } from '@clerk/nextjs';
import { ListTodo, Cloud } from 'lucide-react';
import { ProviderWhitelist } from './ProviderWhitelist';
import { CloudSettings } from './CloudSettings';
export const Org = () => {
const t = useTranslations('OrganizationProfile');
return (
<div className="mx-auto">
<OrganizationProfile
routing="path"
path="/org"
afterLeaveOrganizationUrl="/select-org"
>
<OrganizationProfile.Page
label={t('provider_whitelist')}
url="provider-whitelist"
labelIcon={<ListTodo className="size-4" />}
>
<ProviderWhitelist />
</OrganizationProfile.Page>
<OrganizationProfile.Page
label={t('cloud_settings')}
url="cloud-settings"
labelIcon={<Cloud className="size-4" />}
>
<CloudSettings />
</OrganizationProfile.Page>
</OrganizationProfile>
</div>
);
};

View file

@ -1,335 +0,0 @@
'use client';
import { useTranslations } from 'next-intl';
import { useMemo, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { type ProviderName } from '@roo-code/types';
import { type OrganizationSettings, ORGANIZATION_ALLOW_ALL } from '@/types';
import {
getOrganizationSettings,
updateOrganization,
} from '@/actions/organizationSettings';
import { Badge, Button, Checkbox, Label } from '@/components/ui';
import MultipleSelector from '@/components/ui/multiple-selector';
import openRouterModels from '@/lib/data/openrouter-models.json';
import anthropicModels from '@/lib/data/anthropic.json';
import geminiModels from '@/lib/data/gemini.json';
import deepseekModels from '@/lib/data/deepseek.json';
import openaiNativeModels from '@/lib/data/openai-native.json';
import vertexModels from '@/lib/data/vertex.json';
import bedrockModels from '@/lib/data/bedrock.json';
import mistralModels from '@/lib/data/mistral.json';
import requestyModels from '@/lib/data/requesty-models.json';
import groqModels from '@/lib/data/groq.json';
import xaiModels from '@/lib/data/xai.json';
import unboundModels from '@/lib/data/unbound.json';
import glamaModels from '@/lib/data/glama.json';
import chutesModels from '@/lib/data/chutes.json';
type BaseProvider = {
id: ProviderName;
label: string;
models: string[];
};
const providers = (
[
{ id: 'openrouter', label: 'OpenRouter', models: openRouterModels },
{ id: 'anthropic', label: 'Anthropic', models: anthropicModels },
{ id: 'gemini', label: 'Google Gemini', models: geminiModels },
{ id: 'deepseek', label: 'DeepSeek', models: deepseekModels },
{ id: 'openai-native', label: 'OpenAI', models: openaiNativeModels },
{ id: 'openai', label: 'OpenAI Compatible', models: [] },
{ id: 'vertex', label: 'GCP Vertex AI', models: vertexModels },
{ id: 'bedrock', label: 'Amazon Bedrock', models: bedrockModels },
{ id: 'glama', label: 'Glama', models: glamaModels },
{ id: 'vscode-lm', label: 'VS Code LM API', models: [] },
{ id: 'mistral', label: 'Mistral', models: mistralModels },
{ id: 'lmstudio', label: 'LM Studio', models: [] },
{ id: 'ollama', label: 'Ollama', models: [] },
{ id: 'unbound', label: 'Unbound', models: unboundModels },
{ id: 'requesty', label: 'Requesty', models: requestyModels },
{ id: 'human-relay', label: 'Human Relay', models: [] },
{ id: 'xai', label: 'xAI (Grok)', models: xaiModels },
{ id: 'groq', label: 'Groq', models: groqModels },
{ id: 'chutes', label: 'Chutes AI', models: chutesModels },
{ id: 'litellm', label: 'LiteLLM', models: [] },
] satisfies BaseProvider[]
).sort((a, b) => a.label.localeCompare(b.label));
type ProviderWhitelistFormProps = {
orgSettings: OrganizationSettings;
queryClient: ReturnType<typeof useQueryClient>;
};
const ProviderWhitelistForm = ({
orgSettings,
queryClient,
}: ProviderWhitelistFormProps) => {
const t = useTranslations('ProviderWhitelist');
const fullProviderMetadata = useMemo(
() =>
providers.map((provider) => {
const models = orgSettings.allowList.providers[provider.id]?.models;
if (models) {
const providerModels = new Set(provider.models);
const difference = models.filter(
(model) => !providerModels.has(model),
);
if (difference.length > 0) {
return {
...provider,
models: [...provider.models, ...difference],
};
}
}
return provider;
}),
[orgSettings],
);
const [allowAll, setAllowAll] = useState(orgSettings.allowList.allowAll);
const [providerAllowAll, setProviderAllowAll] = useState(
Object.entries(orgSettings.allowList.providers).reduce(
(acc, [provider, providerSettings]) => {
if (providerSettings.allowAll) {
acc.add(provider);
}
return acc;
},
new Set<string>(),
),
);
const [providerModels, setProviderModels] = useState(
fullProviderMetadata.reduce((acc, meta) => {
acc.set(meta.id, orgSettings.allowList.providers[meta.id]?.models || []);
return acc;
}, new Map<ProviderName, string[]>()),
);
const [hasChanges, setHasChanges] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const isProviderAllowAll = (providerId: ProviderName) =>
providerAllowAll.has(providerId);
const toggleAllowAll = () => {
setAllowAll(!allowAll);
setHasChanges(true);
};
const toggleProvider = (providerId: ProviderName) => {
const newProviderAllowAll = new Set(providerAllowAll);
if (providerAllowAll.has(providerId)) {
newProviderAllowAll.delete(providerId);
} else {
newProviderAllowAll.add(providerId);
}
setProviderAllowAll(newProviderAllowAll);
setHasChanges(true);
};
const setModels = (providerId: ProviderName, models: string[]) => {
const newProviderModels = new Map(providerModels);
newProviderModels.set(providerId, models);
setProviderModels(newProviderModels);
setHasChanges(true);
};
const saveChanges = async () => {
setIsSaving(true);
try {
let allowList;
if (allowAll) {
allowList = ORGANIZATION_ALLOW_ALL;
} else {
allowList = {
allowAll: false,
providers: fullProviderMetadata.reduce(
(acc, meta) => {
if (providerAllowAll.has(meta.id)) {
acc[meta.id] = {
allowAll: true,
};
} else {
const models = providerModels.get(meta.id);
if (models && models.length > 0) {
acc[meta.id] = {
allowAll: false,
models: models,
};
}
}
return acc;
},
{} as Record<
ProviderName,
{
allowAll: boolean;
models?: string[];
}
>,
),
};
}
const result = await updateOrganization({
allowList: allowList,
});
if (!result.success) {
throw new Error(result.error || 'Failed to update settings');
}
queryClient.invalidateQueries({ queryKey: ['organizationSettings'] });
toast.success('Provider settings saved successfully');
setHasChanges(false);
} catch (error) {
console.error('Failed to save provider settings', error);
toast.error('Failed to save provider settings');
} finally {
setIsSaving(false);
}
};
return (
<>
<div className="cl-header 🔒️ cl-internal-qo3qk7">
<div className="cl-internal-1pr5xvn">
<h1 className="cl-headerTitle 🔒️ cl-internal-190cjq9">
{t('providers_section_title')}
</h1>
</div>
</div>
<p className="mb-6 text-sm text-muted-foreground">
{t('providers_section_description')}
</p>
<div className="space-y-8">
<div className="space-y-6">
<div className="mb-4 flex items-center space-x-2">
<Checkbox
id="allow-all-providers"
checked={allowAll}
onCheckedChange={toggleAllowAll}
disabled={isSaving}
/>
<Label
htmlFor="allow-all-providers"
className="text-sm font-medium"
>
Allow All Providers
</Label>
</div>
<div className={allowAll ? 'opacity-50' : ''}>
{fullProviderMetadata.map((provider) => (
<div key={provider.id} className="mb-4">
<div className="mb-2 flex items-center justify-between">
<div className="text-sm font-medium">{provider.label}</div>
<div className="flex items-center space-x-2">
<Label
htmlFor={`provider-${provider.id}`}
className="text-sm"
>
Allow all models
</Label>
<Checkbox
id={`provider-${provider.id}`}
checked={isProviderAllowAll(provider.id)}
disabled={allowAll || isSaving}
onCheckedChange={() => toggleProvider(provider.id)}
/>
</div>
</div>
<div className="max-w-full">
<MultipleSelector
defaultOptions={provider.models.map((model) => ({
label: model,
value: model,
disable: providerModels.get(provider.id)?.includes(model),
}))}
value={(providerModels.get(provider.id) || []).map(
(model) => ({
label: model,
value: model,
}),
)}
creatable
disabled={allowAll || isSaving}
placeholder="Pick models..."
onChange={(options) =>
setModels(
provider.id,
options.map((option) => option.value),
)
}
/>
</div>
</div>
))}
</div>
<div className="flex items-center space-x-2 pt-2">
<Badge variant="outline" className="text-xs">
{`Policy v${orgSettings?.version || 1}`}
</Badge>
<span className="text-xs text-muted-foreground">
{isSaving
? 'Updating provider whitelist...'
: hasChanges
? 'You have unsaved changes'
: 'Changes will be pushed to SSE stream within 30 seconds'}
</span>
</div>
<Button
onClick={saveChanges}
disabled={!hasChanges || isSaving}
className="mt-4"
>
{isSaving ? (
<>
<span className="mr-2">Saving...</span>
<span className="animate-spin"></span>
</>
) : (
'Save Changes'
)}
</Button>
</div>
</div>
</>
);
};
export const ProviderWhitelist = () => {
const queryClient = useQueryClient();
const { data: orgSettings } = useQuery({
queryKey: ['getOrganizationSettings'],
queryFn: getOrganizationSettings,
});
if (!orgSettings) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin text-2xl"></div>
<span className="ml-2">Loading settings...</span>
</div>
);
}
return (
<ProviderWhitelistForm
orgSettings={orgSettings}
queryClient={queryClient}
/>
);
};

View file

@ -1,5 +1,13 @@
import { Org } from './Org';
import { OrganizationProfile } from '@clerk/nextjs';
export default function Page() {
return <Org />;
return (
<div className="mx-auto">
<OrganizationProfile
routing="path"
path="/org"
afterLeaveOrganizationUrl="/select-org"
/>
</div>
);
}

View file

@ -0,0 +1,227 @@
import { useMemo, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import type { ProviderName } from '@roo-code/types';
import { type OrganizationSettings, ORGANIZATION_ALLOW_ALL } from '@/types';
import { providers } from '@/lib/providers';
import { updateOrganization } from '@/actions/organizationSettings';
import { Badge, Button, Checkbox, Label } from '@/components/ui';
import { MultipleSelector } from '@/components/ui/ecosystem';
import { Loading } from '@/components/layout';
type ProviderFormProps = {
orgSettings: OrganizationSettings;
};
export const ProviderForm = ({ orgSettings }: ProviderFormProps) => {
const queryClient = useQueryClient();
const fullProviderMetadata = useMemo(
() =>
providers.map((provider) => {
const models = orgSettings.allowList.providers[provider.id]?.models;
if (models) {
const providerModels = new Set(provider.models);
const difference = models.filter(
(model) => !providerModels.has(model),
);
if (difference.length > 0) {
return {
...provider,
models: [...provider.models, ...difference],
};
}
}
return provider;
}),
[orgSettings],
);
const [allowAll, setAllowAll] = useState(orgSettings.allowList.allowAll);
const [providerAllowAll, setProviderAllowAll] = useState(
Object.entries(orgSettings.allowList.providers).reduce(
(acc, [provider, providerSettings]) => {
if (providerSettings.allowAll) {
acc.add(provider);
}
return acc;
},
new Set<string>(),
),
);
const [providerModels, setProviderModels] = useState(
fullProviderMetadata.reduce((acc, meta) => {
acc.set(meta.id, orgSettings.allowList.providers[meta.id]?.models || []);
return acc;
}, new Map<ProviderName, string[]>()),
);
const [hasChanges, setHasChanges] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const isProviderAllowAll = (providerId: ProviderName) =>
providerAllowAll.has(providerId);
const toggleAllowAll = () => {
setAllowAll(!allowAll);
setHasChanges(true);
};
const toggleProvider = (providerId: ProviderName) => {
const newProviderAllowAll = new Set(providerAllowAll);
if (providerAllowAll.has(providerId)) {
newProviderAllowAll.delete(providerId);
} else {
newProviderAllowAll.add(providerId);
}
setProviderAllowAll(newProviderAllowAll);
setHasChanges(true);
};
const setModels = (providerId: ProviderName, models: string[]) => {
const newProviderModels = new Map(providerModels);
newProviderModels.set(providerId, models);
setProviderModels(newProviderModels);
setHasChanges(true);
};
const saveChanges = async () => {
setIsSaving(true);
try {
let allowList;
if (allowAll) {
allowList = ORGANIZATION_ALLOW_ALL;
} else {
allowList = {
allowAll: false,
providers: fullProviderMetadata.reduce(
(acc, meta) => {
if (providerAllowAll.has(meta.id)) {
acc[meta.id] = { allowAll: true };
} else {
const models = providerModels.get(meta.id);
if (models && models.length > 0) {
acc[meta.id] = { allowAll: false, models: models };
}
}
return acc;
},
{} as Record<
ProviderName,
{ allowAll: boolean; models?: string[] }
>,
),
};
}
const result = await updateOrganization({ allowList: allowList });
if (!result.success) {
throw new Error(result.error || 'Failed to update settings');
}
queryClient.invalidateQueries({ queryKey: ['organizationSettings'] });
toast.success('Provider settings saved successfully');
setHasChanges(false);
} catch (error) {
console.error('Failed to save provider settings', error);
toast.error('Failed to save provider settings');
} finally {
setIsSaving(false);
}
};
return (
<>
<div className="mb-4 flex items-center space-x-2">
<Checkbox
id="allow-all-providers"
checked={allowAll}
onCheckedChange={toggleAllowAll}
disabled={isSaving}
/>
<Label htmlFor="allow-all-providers" className="text-sm font-medium">
Allow All Providers
</Label>
</div>
<div className={allowAll ? 'opacity-50' : ''}>
{fullProviderMetadata.map((provider) => (
<div key={provider.id} className="mb-4">
<div className="mb-2 flex items-center justify-between">
<div className="text-sm font-medium">{provider.label}</div>
<div className="flex items-center space-x-2">
<Label htmlFor={`provider-${provider.id}`} className="text-sm">
Allow all models
</Label>
<Checkbox
id={`provider-${provider.id}`}
checked={isProviderAllowAll(provider.id)}
disabled={allowAll || isSaving}
onCheckedChange={() => toggleProvider(provider.id)}
/>
</div>
</div>
<div className="max-w-full">
<MultipleSelector
defaultOptions={provider.models.map((model) => ({
label: model,
value: model,
disable: providerModels.get(provider.id)?.includes(model),
}))}
value={(providerModels.get(provider.id) || []).map((model) => ({
label: model,
value: model,
}))}
creatable
disabled={allowAll || isSaving}
placeholder="Pick models..."
onChange={(options) =>
setModels(
provider.id,
options.map((option) => option.value),
)
}
/>
</div>
</div>
))}
</div>
<div className="flex items-center space-x-2 pt-2">
<Badge variant="outline" className="text-xs">
{`Policy v${orgSettings?.version || 1}`}
</Badge>
<span className="text-xs text-muted-foreground">
{isSaving
? 'Updating provider whitelist...'
: hasChanges
? 'You have unsaved changes'
: 'Changes will be pushed to SSE stream within 30 seconds'}
</span>
</div>
<Button
onClick={saveChanges}
disabled={!hasChanges || isSaving}
className="mt-4"
>
{isSaving ? <Loading /> : 'Save Changes'}
</Button>
</>
);
};

View file

@ -0,0 +1,31 @@
'use client';
import { useTranslations } from 'next-intl';
import { useQuery } from '@tanstack/react-query';
import { getOrganizationSettings } from '@/actions/organizationSettings';
import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui';
import { Loading } from '@/components/layout';
import { ProviderForm } from './ProviderForm';
export const ProviderSettings = () => {
const t = useTranslations('ProviderSettings');
const { data: orgSettings } = useQuery({
queryKey: ['getOrganizationSettings'],
queryFn: getOrganizationSettings,
});
return (
<>
<Card>
<CardHeader>
<CardTitle>{t('title')}</CardTitle>
<CardDescription>{t('description')}</CardDescription>
</CardHeader>
</Card>
{orgSettings ? <ProviderForm orgSettings={orgSettings} /> : <Loading />}
</>
);
};

View file

@ -0,0 +1,5 @@
import { ProviderSettings } from './ProviderSettings';
export default function Page() {
return <ProviderSettings />;
}

View file

@ -0,0 +1,111 @@
'use client';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Cloud } from 'lucide-react';
import type { OrganizationSettings, OrganizationCloudSettings } from '@/types';
import { updateOrganization } from '@/actions/organizationSettings';
import {
Button,
Checkbox,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from '@/components/ui';
import { Loading } from '@/components/layout';
type FormData = {
recordTaskMessages: boolean;
};
type TelemetryFormProps = {
orgSettings: OrganizationSettings;
};
export const TelemetryForm = ({ orgSettings }: TelemetryFormProps) => {
const queryClient = useQueryClient();
const t = useTranslations('TelemetrySettings');
const [isSaving, setIsSaving] = useState(false);
const form = useForm<FormData>({
defaultValues: {
recordTaskMessages:
orgSettings.cloudSettings?.recordTaskMessages ?? false,
},
});
const onSubmit = async (data: FormData) => {
setIsSaving(true);
try {
const cloudSettings: OrganizationCloudSettings = {
recordTaskMessages: data.recordTaskMessages,
};
const result = await updateOrganization({ cloudSettings });
if (result.success) {
queryClient.invalidateQueries({
queryKey: ['getOrganizationSettings'],
});
toast.success('Cloud settings saved successfully');
} else {
throw new Error(result.error || 'An unexpected error occurred.');
}
} catch (error) {
console.error('Failed to update cloud settings:', error);
toast.error('Failed to save cloud settings. Please try again.');
} finally {
setIsSaving(false);
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4 rounded-lg p-4">
<div className="flex items-center gap-2">
<Cloud className="size-5" />
<h2 className="text-lg font-medium">Task Recording</h2>
</div>
<div className="mt-4 space-y-4">
<FormField
control={form.control}
name="recordTaskMessages"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
disabled={isSaving}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>{t('record_task_messages')}</FormLabel>
<FormDescription>
{t('record_task_messages_description')}
</FormDescription>
</div>
</FormItem>
)}
/>
</div>
</div>
<div className="flex justify-end space-x-4 border-t pt-4">
<Button type="submit" disabled={isSaving}>
{isSaving ? <Loading /> : 'Save Changes'}
</Button>
</div>
</form>
</Form>
);
};

View file

@ -0,0 +1,31 @@
'use client';
import { useTranslations } from 'next-intl';
import { useQuery } from '@tanstack/react-query';
import { getOrganizationSettings } from '@/actions/organizationSettings';
import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui';
import { Loading } from '@/components/layout';
import { TelemetryForm } from './TelemetryForm';
export const TelemetrySettings = () => {
const t = useTranslations('TelemetrySettings');
const { data: orgSettings } = useQuery({
queryKey: ['getOrganizationSettings'],
queryFn: getOrganizationSettings,
});
return (
<>
<Card>
<CardHeader>
<CardTitle>{t('title')}</CardTitle>
<CardDescription>{t('description')}</CardDescription>
</CardHeader>
</Card>
{orgSettings ? <TelemetryForm orgSettings={orgSettings} /> : <Loading />}
</>
);
};

View file

@ -0,0 +1,5 @@
import { TelemetrySettings } from './TelemetrySettings';
export default function Page() {
return <TelemetrySettings />;
}

View file

@ -6,7 +6,7 @@ import { useAuth } from '@clerk/nextjs';
import { type DeveloperUsage, getDeveloperUsage } from '@/actions/analytics';
import { formatCurrency, formatNumber } from '@/lib/formatters';
import { Button, Skeleton } from '@/components/ui';
import { DataTable } from '@/components/layout/DataTable';
import { DataTable } from '@/components/layout';
import type { Filter } from './types';

View file

@ -6,7 +6,7 @@ import { useAuth } from '@clerk/nextjs';
import { type ModelUsage, getModelUsage } from '@/actions/analytics';
import { formatCurrency, formatNumber } from '@/lib/formatters';
import { Button, Skeleton } from '@/components/ui';
import { DataTable } from '@/components/layout/DataTable';
import { DataTable } from '@/components/layout';
import type { Filter } from './types';

View file

@ -8,7 +8,7 @@ import type { Task } from '@/types/analytics';
import { getTasks } from '@/actions/analytics';
import { formatNumber, formatCurrency } from '@/lib/formatters';
import { Button, Skeleton } from '@/components/ui';
import { DataTable } from '@/components/layout/DataTable';
import { DataTable } from '@/components/layout';
import type { Filter } from './types';
import { Status } from './Status';

View file

@ -44,7 +44,7 @@ export default function Page() {
}, [router, isLoading, isSignedIn, orgId, authState.params]);
return (
<div className="flex flex-row items-center gap-2">
<div className="flex justify-center">
{isLoading ? (
<LoaderCircle className="animate-spin" />
) : isSignedIn ? (

View file

@ -28,11 +28,7 @@ export const SelectOrg = () => {
userSuggestions.isLoading;
if (isLoading) {
return (
<div className="flex flex-row items-center gap-2">
<LoaderCircle className="animate-spin" />
</div>
);
return <LoaderCircle className="animate-spin" />;
}
const isBlocked =

View file

@ -0,0 +1,7 @@
import { LoaderCircle } from 'lucide-react';
export const Loading = () => (
<div className="flex items-center justify-center">
<LoaderCircle className="animate-spin" />
</div>
);

View file

@ -13,6 +13,8 @@ const tabValues = [
'/dashboard',
'/usage',
'/audit-logs',
'/providers',
'/telemetry',
'/org',
'/hidden',
] as const;
@ -51,6 +53,8 @@ export const NavbarMenu = (props: NavbarMenuProps) => {
<TabsTrigger value="/dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="/usage">Usage</TabsTrigger>
<TabsTrigger value="/audit-logs">Audit Logs</TabsTrigger>
<TabsTrigger value="/providers">Providers</TabsTrigger>
<TabsTrigger value="/telemetry">Telemetry</TabsTrigger>
<TabsTrigger value="/org">Organization</TabsTrigger>
<TabsTrigger value="/hidden" className="hidden" />
</TabsList>

View file

@ -5,6 +5,8 @@ export { Logo, HoppingLogo } from './Logo';
export { NavbarHeader } from './NavbarHeader';
export { NavbarMenu } from './NavbarMenu';
export { Connected } from './Connected';
export { Loading } from './Loading';
export { DataTable } from './DataTable';
export { ThemeProvider } from './ThemeProvider';
export { AuthProvider } from './AuthProvider';

View file

@ -1,2 +1,3 @@
export * from './button';
export * from './multiple-selector';
export * from './tabs';

View file

@ -1,18 +1,18 @@
'use client';
import { Command as CommandPrimitive, useCommandState } from 'cmdk';
import { X } from 'lucide-react';
import * as React from 'react';
import { forwardRef, useEffect } from 'react';
import { Command as CommandPrimitive, useCommandState } from 'cmdk';
import { X } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import {
Badge,
Command,
CommandGroup,
CommandItem,
CommandList,
} from '@/components/ui/command';
import { cn } from '@/lib/utils';
} from '@/components/ui';
export interface Option {
value: string;
@ -176,7 +176,7 @@ const CommandEmpty = forwardRef<
CommandEmpty.displayName = 'CommandEmpty';
const MultipleSelector = React.forwardRef<
export const MultipleSelector = React.forwardRef<
MultipleSelectorRef,
MultipleSelectorProps
>(
@ -634,4 +634,3 @@ const MultipleSelector = React.forwardRef<
);
MultipleSelector.displayName = 'MultipleSelector';
export default MultipleSelector;

View file

@ -1,5 +1,6 @@
export * from './badge';
export * from './button';
export * from './command';
export * from './card';
export * from './checkbox';
export * from './drawer';

View file

@ -20,17 +20,15 @@
"cloud_settings": "Cloud Settings",
"default_parameters": "Default Parameters"
},
"ProviderWhitelist": {
"title": "Provider Whitelist & Default Parameters",
"description": "Control which AI providers are allowed and set default parameters for your organization",
"providers_section_title": "Allowed Providers",
"providers_section_description": "Select which AI providers and models are allowed to be used",
"ProviderSettings": {
"title": "Provider Settings",
"description": "Select which AI providers and models are allowed to be used",
"parameters_section_title": "Default Parameters",
"parameters_section_description": "Set global default parameters for AI model calls"
},
"CloudSettings": {
"cloud_section_title": "Cloud Settings",
"cloud_section_description": "Configure how your organization's data is handled in the cloud.",
"TelemetrySettings": {
"title": "Telemetry Settings",
"description": "Configure how your organization's data is handled in the cloud.",
"record_task_messages": "Record task messages",
"record_task_messages_description": "When enabled task messages and interactions will be recorded."
},

View file

@ -20,17 +20,15 @@
"cloud_settings": "Paramètres Cloud",
"default_parameters": "Paramètres par défaut"
},
"ProviderWhitelist": {
"title": "Liste blanche des fournisseurs et paramètres par défaut",
"description": "Contrôlez quels fournisseurs d'IA sont autorisés et définissez les paramètres par défaut pour votre organisation",
"providers_section_title": "Fournisseurs autorisés",
"providers_section_description": "Sélectionnez quels fournisseurs et modèles d'IA sont autorisés à être utilisés",
"ProviderSettings": {
"title": "Fournisseurs autorisés",
"description": "Sélectionnez quels fournisseurs et modèles d'IA sont autorisés à être utilisés",
"parameters_section_title": "Paramètres par défaut",
"parameters_section_description": "Définissez les paramètres par défaut globaux pour les appels aux modèles d'IA"
},
"CloudSettings": {
"cloud_section_title": "Paramètres Cloud",
"cloud_section_description": "Configurez la manière dont les données de votre organisation sont gérées dans le cloud.",
"TelemetrySettings": {
"title": "Paramètres Cloud",
"description": "Configurez la manière dont les données de votre organisation sont gérées dans le cloud.",
"record_task_messages": "Enregistrer les messages de tâche",
"record_task_messages_description": "Lorsque cette option est activée, les messages et interactions de tâche seront enregistrés."
},

47
src/lib/providers.ts Normal file
View file

@ -0,0 +1,47 @@
import { type ProviderName } from '@roo-code/types';
import openRouterModels from '@/lib/data/openrouter-models.json';
import anthropicModels from '@/lib/data/anthropic.json';
import geminiModels from '@/lib/data/gemini.json';
import deepseekModels from '@/lib/data/deepseek.json';
import openaiNativeModels from '@/lib/data/openai-native.json';
import vertexModels from '@/lib/data/vertex.json';
import bedrockModels from '@/lib/data/bedrock.json';
import mistralModels from '@/lib/data/mistral.json';
import requestyModels from '@/lib/data/requesty-models.json';
import groqModels from '@/lib/data/groq.json';
import xaiModels from '@/lib/data/xai.json';
import unboundModels from '@/lib/data/unbound.json';
import glamaModels from '@/lib/data/glama.json';
import chutesModels from '@/lib/data/chutes.json';
type BaseProvider = {
id: ProviderName;
label: string;
models: string[];
};
export const providers = (
[
{ id: 'openrouter', label: 'OpenRouter', models: openRouterModels },
{ id: 'anthropic', label: 'Anthropic', models: anthropicModels },
{ id: 'gemini', label: 'Google Gemini', models: geminiModels },
{ id: 'deepseek', label: 'DeepSeek', models: deepseekModels },
{ id: 'openai-native', label: 'OpenAI', models: openaiNativeModels },
{ id: 'openai', label: 'OpenAI Compatible', models: [] },
{ id: 'vertex', label: 'GCP Vertex AI', models: vertexModels },
{ id: 'bedrock', label: 'Amazon Bedrock', models: bedrockModels },
{ id: 'glama', label: 'Glama', models: glamaModels },
{ id: 'vscode-lm', label: 'VS Code LM API', models: [] },
{ id: 'mistral', label: 'Mistral', models: mistralModels },
{ id: 'lmstudio', label: 'LM Studio', models: [] },
{ id: 'ollama', label: 'Ollama', models: [] },
{ id: 'unbound', label: 'Unbound', models: unboundModels },
{ id: 'requesty', label: 'Requesty', models: requestyModels },
{ id: 'human-relay', label: 'Human Relay', models: [] },
{ id: 'xai', label: 'xAI (Grok)', models: xaiModels },
{ id: 'groq', label: 'Groq', models: groqModels },
{ id: 'chutes', label: 'Chutes AI', models: chutesModels },
{ id: 'litellm', label: 'LiteLLM', models: [] },
] satisfies BaseProvider[]
).sort((a, b) => a.label.localeCompare(b.label));