Add cloud settings (#49)

These are meant to be organization settings about roo code cloud, rather
than cloud managed non-cloud settings for the extension.
This commit is contained in:
John Richmond 2025-05-29 20:48:16 -07:00 committed by GitHub
parent 6a65576136
commit cdb9734b29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 738 additions and 14 deletions

View file

@ -11,6 +11,7 @@ import {
type OrganizationSettings,
organizationAllowListSchema,
organizationDefaultSettingsSchema,
organizationCloudSettingsSchema,
} from '@/types';
import { AuditLogTargetType, client as db, orgSettings } from '@/db/server';
import { handleError, isAuthSuccess } from '@/lib/server';
@ -18,9 +19,7 @@ import { handleError, isAuthSuccess } from '@/lib/server';
import { validateAuth } from './auth';
import { insertAuditLog } from './auditLogs';
export async function getOrganizationSettings(): Promise<
OrganizationSettings | undefined
> {
export async function getOrganizationSettings(): Promise<OrganizationSettings> {
const { userId, orgId } = await auth();
if (!userId) {
@ -37,7 +36,7 @@ export async function getOrganizationSettings(): Promise<
.where(eq(orgSettings.orgId, orgId))
.limit(1);
return settings.length === 0 ? ORGANIZATION_DEFAULT : settings[0];
return settings[0] || ORGANIZATION_DEFAULT;
}
/**
@ -47,12 +46,16 @@ const updateOrganizationSchema = z
.object({
defaultSettings: organizationDefaultSettingsSchema.optional(),
allowList: organizationAllowListSchema.optional(),
cloudSettings: organizationCloudSettingsSchema.optional(),
})
.refine(
(data) =>
data.defaultSettings !== undefined || data.allowList !== undefined,
data.defaultSettings !== undefined ||
data.allowList !== undefined ||
data.cloudSettings !== undefined,
{
message: 'At least one of defaultSettings or allowList must be provided',
message:
'At least one of defaultSettings, allowList, or cloudSettings must be provided',
},
);
@ -96,12 +99,17 @@ export async function updateOrganization(
updateData.allowList = validatedData.allowList;
}
if (validatedData.cloudSettings) {
updateData.cloudSettings = validatedData.cloudSettings;
}
if (isNewRecord) {
await tx.insert(orgSettings).values({
orgId,
version: 1,
defaultSettings: validatedData.defaultSettings || {},
allowList: validatedData.allowList || ORGANIZATION_ALLOW_ALL,
cloudSettings: validatedData.cloudSettings || {},
});
} else {
await tx
@ -135,6 +143,17 @@ export async function updateOrganization(
description: 'Updated organization allow list',
});
}
if (validatedData.cloudSettings) {
await insertAuditLog(tx, {
userId,
orgId,
targetType: AuditLogTargetType.CLOUD_SETTINGS,
targetId: 'organization-cloud-settings',
newValue: validatedData.cloudSettings,
description: 'Updated organization cloud settings',
});
}
});
return {

View file

@ -0,0 +1,162 @@
'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

@ -2,10 +2,11 @@
import { useTranslations } from 'next-intl';
import { OrganizationProfile } from '@clerk/nextjs';
import { ListTodo, SlidersHorizontal } from 'lucide-react';
import { ListTodo, SlidersHorizontal, Cloud } from 'lucide-react';
import { DefaultParameters } from './DefaultParameters';
import { ProviderWhitelist } from './ProviderWhitelist';
import { CloudSettings } from './CloudSettings';
export const Org = () => {
const t = useTranslations('OrganizationProfile');
@ -24,6 +25,13 @@ export const Org = () => {
>
<ProviderWhitelist />
</OrganizationProfile.Page>
<OrganizationProfile.Page
label={t('cloud_settings')}
url="cloud-settings"
labelIcon={<Cloud className="size-4" />}
>
<CloudSettings />
</OrganizationProfile.Page>
<OrganizationProfile.Page
label={t('default_parameters')}
url="default-parameters"

View file

@ -1,7 +1,6 @@
import { NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { type OrganizationSettings, ORGANIZATION_ALLOW_ALL } from '@/types';
import { getOrganizationSettings } from '@/actions/organizationSettings';
export async function GET() {
@ -22,12 +21,7 @@ export async function GET() {
);
}
const settings: OrganizationSettings =
(await getOrganizationSettings()) || {
defaultSettings: {},
allowList: ORGANIZATION_ALLOW_ALL,
version: 0,
};
const settings = await getOrganizationSettings();
return NextResponse.json(settings);
} catch (error) {

View file

@ -2,4 +2,5 @@ export enum AuditLogTargetType {
PROVIDER_WHITELIST = 1,
DEFAULT_PARAMETERS = 2,
MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected.
CLOUD_SETTINGS = 4,
}

View file

@ -0,0 +1 @@
ALTER TABLE "organization_settings" ADD COLUMN "cloud_settings" jsonb DEFAULT '{}'::jsonb NOT NULL;

View file

@ -0,0 +1,503 @@
{
"id": "5c95f3bf-5048-4ba1-9f71-2110879fc161",
"prevId": "bef500d1-5175-4068-af51-1f565355f1ac",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.audit_logs": {
"name": "audit_logs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"organization_id": {
"name": "organization_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"target_type": {
"name": "target_type",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"target_id": {
"name": "target_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"new_value": {
"name": "new_value",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"audit_logs_user_id_idx": {
"name": "audit_logs_user_id_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"audit_logs_organization_id_idx": {
"name": "audit_logs_organization_id_idx",
"columns": [
{
"expression": "organization_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"audit_logs_target_idx": {
"name": "audit_logs_target_idx",
"columns": [
{
"expression": "target_type",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "target_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"audit_logs_created_at_idx": {
"name": "audit_logs_created_at_idx",
"columns": [
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"audit_logs_user_id_users_id_fk": {
"name": "audit_logs_user_id_users_id_fk",
"tableFrom": "audit_logs",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"audit_logs_organization_id_organizations_id_fk": {
"name": "audit_logs_organization_id_organizations_id_fk",
"tableFrom": "audit_logs",
"tableTo": "organizations",
"columnsFrom": ["organization_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.organization_settings": {
"name": "organization_settings",
"schema": "",
"columns": {
"organization_id": {
"name": "organization_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"cloud_settings": {
"name": "cloud_settings",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"default_settings": {
"name": "default_settings",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"allow_list": {
"name": "allow_list",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{\"allowAll\":true,\"providers\":{}}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"organization_settings_created_at_idx": {
"name": "organization_settings_created_at_idx",
"columns": [
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"organization_settings_organization_id_organizations_id_fk": {
"name": "organization_settings_organization_id_organizations_id_fk",
"tableFrom": "organization_settings",
"tableTo": "organizations",
"columnsFrom": ["organization_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.organizations": {
"name": "organizations",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"image_url": {
"name": "image_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"entity": {
"name": "entity",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"last_sync_at": {
"name": "last_sync_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"organizations_slug_idx": {
"name": "organizations_slug_idx",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"organizations_created_at_idx": {
"name": "organizations_created_at_idx",
"columns": [
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"organization_id": {
"name": "organization_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"organization_role": {
"name": "organization_role",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"image_url": {
"name": "image_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"entity": {
"name": "entity",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"last_sync_at": {
"name": "last_sync_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"users_organization_id_idx": {
"name": "users_organization_id_idx",
"columns": [
{
"expression": "organization_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"users_organization_role_idx": {
"name": "users_organization_role_idx",
"columns": [
{
"expression": "organization_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "organization_role",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"users_email_idx": {
"name": "users_email_idx",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"users_created_at_idx": {
"name": "users_created_at_idx",
"columns": [
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"users_organization_id_organizations_id_fk": {
"name": "users_organization_id_organizations_id_fk",
"tableFrom": "users",
"tableTo": "organizations",
"columnsFrom": ["organization_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -29,6 +29,13 @@
"when": 1747551227749,
"tag": "0003_strange_wild_pack",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1748555520729,
"tag": "0004_overjoyed_bucky",
"breakpoints": true
}
]
}

View file

@ -13,6 +13,7 @@ import {
type OrganizationDefaultSettings,
type OrganizationAllowList,
ORGANIZATION_ALLOW_ALL,
type OrganizationCloudSettings,
} from '@/types';
import { AuditLogTargetType } from './enums';
@ -93,6 +94,10 @@ export const orgSettings = pgTable(
.primaryKey()
.references(() => orgs.id), // Assigned by Clerk.
version: integer('version').notNull().default(1),
cloudSettings: jsonb('cloud_settings')
.notNull()
.$type<OrganizationCloudSettings>()
.default({}),
defaultSettings: jsonb('default_settings')
.notNull()
.$type<OrganizationDefaultSettings>()

View file

@ -17,6 +17,7 @@
},
"OrganizationProfile": {
"provider_whitelist": "Provider Whitelist",
"cloud_settings": "Cloud Settings",
"default_parameters": "Default Parameters"
},
"ProviderWhitelist": {
@ -27,6 +28,12 @@
"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.",
"record_task_messages": "Record task messages",
"record_task_messages_description": "When enabled task messages and interactions will be recorded."
},
"Analytics": {
"view_mode_title": "View By",
"view_mode_developers": "Developers",

View file

@ -17,6 +17,7 @@
},
"OrganizationProfile": {
"provider_whitelist": "Liste blanche des fournisseurs",
"cloud_settings": "Paramètres Cloud",
"default_parameters": "Paramètres par défaut"
},
"ProviderWhitelist": {
@ -27,6 +28,12 @@
"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.",
"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."
},
"Analytics": {
"view_mode_title": "Afficher Par",
"view_mode_developers": "Développeurs",

View file

@ -40,8 +40,17 @@ export type OrganizationDefaultSettings = z.infer<
typeof organizationDefaultSettingsSchema
>;
export const organizationCloudSettingsSchema = z.object({
recordTaskMessages: z.boolean().optional(),
});
export type OrganizationCloudSettings = z.infer<
typeof organizationCloudSettingsSchema
>;
export const organizationSettingsSchema = z.object({
version: z.number(),
cloudSettings: organizationCloudSettingsSchema.optional(),
defaultSettings: organizationDefaultSettingsSchema,
allowList: organizationAllowListSchema,
});
@ -50,6 +59,7 @@ export type OrganizationSettings = z.infer<typeof organizationSettingsSchema>;
export const ORGANIZATION_DEFAULT: OrganizationSettings = {
version: 0,
cloudSettings: {},
defaultSettings: {},
allowList: ORGANIZATION_ALLOW_ALL,
} as const;