From 1feafb604b63a9465e3a089726e7dce1698dd233 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 4 Sep 2025 22:18:13 -0600 Subject: [PATCH 1/4] fix(cmdk): add back docs ai endpoint (#38453) * fix(cmdk): add back docs ai endpoint * fix: add /ai/docs back to middleware whitelist --- apps/studio/.env | 2 +- apps/studio/middleware.ts | 1 + apps/studio/pages/api/ai/docs.ts | 153 +++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 apps/studio/pages/api/ai/docs.ts diff --git a/apps/studio/.env b/apps/studio/.env index addb3f02120a2..aa475c4b29833 100644 --- a/apps/studio/.env +++ b/apps/studio/.env @@ -21,7 +21,7 @@ NEXT_PUBLIC_GOTRUE_URL=$SUPABASE_PUBLIC_URL/auth/v1 NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 # CmdK / AI -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzUwOTQ4MzUsImV4cCI6MTk5MDY3MDgzNX0.0PMlOxtKL4O9GGZuAP_Xl4f-Tut1qOnW4bNEmAtoB8w +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTgzNzc1MTgsImV4cCI6MjAzMzk1MzUxOH0.aIqjQ9V7djMxYit-DT1fYNV_VWMHSqldh_18XfX2_BE NEXT_PUBLIC_SUPABASE_URL=https://xguihxuzqibwxjnimxev.supabase.co DOCKER_SOCKET_LOCATION=/var/run/docker.sock diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts index 0e34bbb48f292..5327cb589e3a9 100644 --- a/apps/studio/middleware.ts +++ b/apps/studio/middleware.ts @@ -13,6 +13,7 @@ const HOSTED_SUPPORTED_API_URLS = [ '/ai/sql/title-v2', '/ai/onboarding/design', '/ai/feedback/classify', + '/ai/docs', '/get-ip-address', '/get-utc-time', '/get-deployment-commit', diff --git a/apps/studio/pages/api/ai/docs.ts b/apps/studio/pages/api/ai/docs.ts new file mode 100644 index 0000000000000..21aaf2da7fa06 --- /dev/null +++ b/apps/studio/pages/api/ai/docs.ts @@ -0,0 +1,153 @@ +import { SupabaseClient } from '@supabase/supabase-js' +import { ApplicationError, UserError, clippy } from 'ai-commands/edge' +import { NextRequest } from 'next/server' +import OpenAI from 'openai' + +export const config = { + runtime: 'edge', + /* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that + overlap with the OpenAI API regions. + + Reference for Vercel regions: https://vercel.com/docs/edge-network/regions#region-list + Reference for OpenAI regions: https://help.openai.com/en/articles/5347006-openai-api-supported-countries-and-territories + */ + regions: [ + 'arn1', + 'bom1', + 'cdg1', + 'cle1', + 'cpt1', + 'dub1', + 'fra1', + 'gru1', + 'hnd1', + 'iad1', + 'icn1', + 'kix1', + 'lhr1', + 'pdx1', + 'sfo1', + 'sin1', + 'syd1', + ], +} + +const openAiKey = process.env.OPENAI_API_KEY +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL +const supabaseServiceKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + +export default async function handler(req: NextRequest) { + console.log('AI Docs request received', supabaseUrl, supabaseServiceKey) + if (!openAiKey) { + return new Response( + JSON.stringify({ + error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } + + if (!supabaseUrl) { + return new Response( + JSON.stringify({ + error: + 'No NEXT_PUBLIC_SUPABASE_URL set. Create this environment variable to use AI features.', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } + + if (!supabaseServiceKey) { + return new Response( + JSON.stringify({ + error: + 'No NEXT_PUBLIC_SUPABASE_ANON_KEY set. Create this environment variable to use AI features.', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } + + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req) + default: + return new Response( + JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }), + { + status: 405, + headers: { 'Content-Type': 'application/json', Allow: 'POST' }, + } + ) + } +} + +async function handlePost(request: NextRequest) { + const openai = new OpenAI({ apiKey: openAiKey }) + + const body = await (request.json() as Promise<{ + messages: { content: string; role: 'user' | 'assistant' }[] + }>) + + const { messages } = body + + if (!messages) { + throw new UserError('Missing messages in request data') + } + + const supabaseClient = new SupabaseClient(supabaseUrl!, supabaseServiceKey!) + + try { + const response = await clippy(openai, supabaseClient, messages) + + // Proxy the streamed SSE response from OpenAI + return new Response(response.body, { + headers: { + 'Content-Type': 'text/event-stream', + }, + }) + } catch (error: unknown) { + console.error(error) + if (error instanceof UserError) { + return new Response( + JSON.stringify({ + error: error.message, + data: error.data, + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' }, + } + ) + } else if (error instanceof ApplicationError) { + // Print out application errors with their additional data + console.error(`${error.message}: ${JSON.stringify(error.data)}`) + } else { + // Print out unexpected errors as is to help with debugging + console.error(error) + } + + console.log('Returning generic 500 ApplicationError to client') + + // TODO: include more response info in debug environments + return new Response( + JSON.stringify({ + error: 'There was an error processing your request', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ) + } +} From 8f3a8f02dd446e6335fcb8d3678421737d127c66 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 5 Sep 2025 12:32:14 +0700 Subject: [PATCH 2/4] =?UTF-8?q?only=20trigger=20request=20to=20/clone=20in?= =?UTF-8?q?=20create=20branch=20modal=20when=20modal=20is=20o=E2=80=A6=20(?= =?UTF-8?q?#38454)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit only trigger request to /clone in create branch modal when modal is opened --- .../interfaces/BranchManagement/CreateBranchModal.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index b69109218a9d1..f85465f7c16a3 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -89,7 +89,13 @@ export const CreateBranchModal = () => { useCheckGithubBranchValidity({ onError: () => {}, }) - const { data: cloneBackups, error: cloneBackupsError } = useCloneBackupsQuery({ projectRef }) + const { data: cloneBackups, error: cloneBackupsError } = useCloneBackupsQuery( + { projectRef }, + { + // [Joshen] Only trigger this request when the modal is opened + enabled: showCreateBranchModal, + } + ) const targetVolumeSizeGb = cloneBackups?.target_volume_size_gb ?? 0 const noPhysicalBackups = cloneBackupsError?.message.startsWith( 'Physical backups need to be enabled' From 31ad637e9e3e678c615edfbf8560cb26dac6cdf2 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 5 Sep 2025 16:25:08 +0700 Subject: [PATCH 3/4] Add feature flag to disable /chore requests trigger in CreateBranchModal (#38461) * Add comment * Lol --- .../interfaces/BranchManagement/CreateBranchModal.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index f85465f7c16a3..9ffa0387be786 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -63,6 +63,9 @@ export const CreateBranchModal = () => { const gitlessBranching = useIsBranching2Enabled() const allowDataBranching = useFlag('allowDataBranching') + // [Joshen] This is meant to be short lived while we're figuring out how to control + // requests to this endpoint. Kill switch in case we need to stop the requests + const disableBackupsCheck = useFlag('disableBackupsCheckInCreatebranchmodal') const isProPlanAndUp = selectedOrg?.plan?.id !== 'free' const promptProPlanUpgrade = IS_PLATFORM && !isProPlanAndUp @@ -93,7 +96,7 @@ export const CreateBranchModal = () => { { projectRef }, { // [Joshen] Only trigger this request when the modal is opened - enabled: showCreateBranchModal, + enabled: showCreateBranchModal && !disableBackupsCheck, } ) const targetVolumeSizeGb = cloneBackups?.target_volume_size_gb ?? 0 @@ -364,13 +367,13 @@ export const CreateBranchModal = () => { - {noPhysicalBackups && ( + {!disableBackupsCheck && noPhysicalBackups && ( PITR is required for the project to clone data into the branch From dee5ff119422996c7d15d9b496020c32f8835ba7 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:49:08 +0200 Subject: [PATCH 4/4] edge function report: cleanup and url filters (#38446) * rm dupped value * use zod in reportsnumericfilter * use zod in reportsselectfilter * use nuqs in filters * validate data before change * unnecessary changes * fix some type errs update tests * use command to add search to select filter * update styles / clear btn in num filter * add search to edge fn report * fix clickable area * fix test * default true * fix availablein * sum paddin * fix default values in tests --- .../interfaces/Reports/Reports.constants.ts | 6 +- .../interfaces/Reports/v2/ReportChartV2.tsx | 4 +- .../Reports/v2/ReportsNumericFilter.tsx | 65 +++++----- .../Reports/v2/ReportsSelectFilter.tsx | 113 +++++++++--------- .../ui/Charts/useChartHoverState.test.tsx | 10 +- .../ui/Charts/useChartHoverState.tsx | 4 +- .../data/reports/v2/edge-functions.config.ts | 28 ++--- .../data/reports/v2/edge-functions.test.tsx | 71 +++++------ .../project/[ref]/reports/edge-functions.tsx | 43 +++++-- 9 files changed, 164 insertions(+), 180 deletions(-) diff --git a/apps/studio/components/interfaces/Reports/Reports.constants.ts b/apps/studio/components/interfaces/Reports/Reports.constants.ts index b6cf92f089428..96b3b326d3b2d 100644 --- a/apps/studio/components/interfaces/Reports/Reports.constants.ts +++ b/apps/studio/components/interfaces/Reports/Reports.constants.ts @@ -566,10 +566,6 @@ export const EDGE_FUNCTION_REGIONS = [ key: 'us-west-1', label: 'N. California', }, - { - key: 'ap-northeast-2', - label: 'Seoul', - }, { key: 'us-west-2', label: 'Oregon', @@ -594,4 +590,4 @@ export const EDGE_FUNCTION_REGIONS = [ key: 'sa-east-1', label: 'São Paulo', }, -] +] as const diff --git a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx index 52c2033dcbaf5..c021a855ac6a3 100644 --- a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx +++ b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx @@ -40,7 +40,7 @@ export const ReportChartV2 = ({ const isAvailable = report.availableIn === undefined || (orgPlanId && report.availableIn.includes(orgPlanId)) - const canFetch = orgPlanId !== undefined + const canFetch = orgPlanId !== undefined && isAvailable const { data: queryResult, @@ -83,7 +83,7 @@ export const ReportChartV2 = ({ const [chartStyle, setChartStyle] = useState(report.defaultChartStyle) - if (!isAvailable && !isLoadingChart) { + if (!isAvailable) { return } diff --git a/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx b/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx index 55173bfe52115..cb3765e1c94fc 100644 --- a/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx +++ b/apps/studio/components/interfaces/Reports/v2/ReportsNumericFilter.tsx @@ -12,8 +12,7 @@ import { } from '@ui/components/shadcn/ui/select' import { Button, cn } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' - -export type ComparisonOperator = '=' | '!=' | '>' | '>=' | '<' | '<=' +import { z } from 'zod' const OPERATOR_LABELS = { '=': 'Equals', @@ -24,15 +23,19 @@ const OPERATOR_LABELS = { '!=': 'Not equal to', } satisfies Record -export interface NumericFilter { - operator: ComparisonOperator - value: number -} +const comparisonOperatorSchema = z.enum(['=', '>=', '<=', '>', '<', '!=']) +export type ComparisonOperator = z.infer + +export const numericFilterSchema = z.object({ + operator: comparisonOperatorSchema, + value: z.number(), +}) +export type NumericFilter = z.infer interface ReportsNumericFilterProps { label: string - value?: NumericFilter - onChange: (value: NumericFilter | undefined) => void + value: NumericFilter | null + onChange: (value: NumericFilter | null) => void operators?: ComparisonOperator[] defaultOperator?: ComparisonOperator placeholder?: string @@ -57,9 +60,9 @@ export const ReportsNumericFilter = ({ className, }: ReportsNumericFilterProps) => { const [open, setOpen] = useState(false) - const [tempValue, setTempValue] = useState(value) + const [tempValue, setTempValue] = useState(value) - const isActive = value !== undefined + const isActive = value !== null useEffect(() => { if (!open) { @@ -67,11 +70,6 @@ export const ReportsNumericFilter = ({ } }, [open, value]) - const handleClear = (e: React.MouseEvent) => { - e.stopPropagation() - onChange(undefined) - } - const handleApply = () => { onChange(tempValue) setOpen(false) @@ -87,27 +85,26 @@ export const ReportsNumericFilter = ({ const handleOperatorChange = (operator: ComparisonOperator) => { setTempValue({ operator, - value: tempValue?.value || 0, + value: tempValue?.value ?? 0, }) } const handleValueChange = (inputValue: string) => { - const numericValue = parseFloat(inputValue) || 0 - setTempValue({ - operator: tempValue?.operator || defaultOperator, - value: numericValue, - }) - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleApply() + if (inputValue === '') { + setTempValue(null) + } else { + const numericValue = parseFloat(inputValue) + if (!isNaN(numericValue)) { + setTempValue({ + operator: tempValue?.operator ?? defaultOperator, + value: numericValue, + }) + } } } const handleClearAll = () => { - setTempValue(undefined) + setTempValue(null) } return ( @@ -133,13 +130,6 @@ export const ReportsNumericFilter = ({ -
- {label} - -
-
{ e.preventDefault() @@ -175,7 +165,6 @@ export const ReportsNumericFilter = ({ placeholder={placeholder} value={tempValue?.value || ''} onChange={(e) => handleValueChange(e.target.value)} - onKeyDown={handleKeyDown} min={min} max={max} step={step} @@ -193,8 +182,8 @@ export const ReportsNumericFilter = ({
- -
-
- {label} - -
-
- -
- {options.length === 0 ? ( -
- {isLoading ? 'Loading options...' : 'No options available'} -
- ) : ( - options.map((option) => ( - - )) - )} -
+ + {showSearch && } + + No options found. + + {options.map((option) => ( + + + + ))} + + +
-
} > -
+
{selectedDateRange && reportConfig .filter((report) => !report.hide)