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/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index b69109218a9d1..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 @@ -89,7 +92,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 && !disableBackupsCheck, + } + ) const targetVolumeSizeGb = cloneBackups?.target_volume_size_gb ?? 0 const noPhysicalBackups = cloneBackupsError?.message.startsWith( 'Physical backups need to be enabled' @@ -358,13 +367,13 @@ export const CreateBranchModal = () => { - {noPhysicalBackups && ( + {!disableBackupsCheck && noPhysicalBackups && ( PITR is required for the project to clone data into the branch 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} - - Clear - - - { 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 = ({ - setOpen(false)} htmlType="button"> - Cancel + + Clear interface ReportsSelectFilterProps { label: string @@ -23,6 +31,7 @@ interface ReportsSelectFilterProps { onChange: (value: SelectFilters) => void isLoading?: boolean className?: string + showSearch?: boolean } export const ReportsSelectFilter = ({ @@ -32,12 +41,12 @@ export const ReportsSelectFilter = ({ onChange, isLoading = false, className, + showSearch = false, }: ReportsSelectFilterProps) => { const [open, setOpen] = useState(false) const [tempValue, setTempValue] = useState(value) - const selectedCount = Object.values(value).filter(Boolean).length - const isActive = selectedCount > 0 + const isActive = tempValue.length > 0 useEffect(() => { if (!open) { @@ -51,7 +60,7 @@ export const ReportsSelectFilter = ({ } const handleClearAll = () => { - setTempValue({}) + setTempValue([]) } const handleKeyDown = (e: React.KeyboardEvent) => { @@ -75,60 +84,52 @@ export const ReportsSelectFilter = ({ > {label} - {selectedCount > 0 && ({selectedCount})} + {tempValue.length > 0 && ({tempValue.length})} - - - {label} - - Clear - - - - - - {options.length === 0 ? ( - - {isLoading ? 'Loading options...' : 'No options available'} - - ) : ( - options.map((option) => ( - - { - setTempValue({ - ...tempValue, - [option.key]: Boolean(checked), - }) - }} - onKeyDown={handleKeyDown} - /> - - {option.label} - {option.description && ( - {option.description} - )} - - - )) - )} - + + {showSearch && } + + No options found. + + {options.map((option) => ( + + + { + setTempValue( + checked + ? [...tempValue, option.value] + : tempValue.filter((x) => x !== option.value) + ) + }} + onKeyDown={handleKeyDown} + /> + + {option.label} + {option.description && ( + {option.description} + )} + + + + ))} + + + - setOpen(false)} htmlType="button"> - Cancel + + Clear { const { result } = renderHook(() => useChartHoverState('chart1')) expect(result.current.hoveredIndex).toBe(null) - expect(result.current.syncHover).toBe(false) - expect(result.current.syncTooltip).toBe(false) + expect(result.current.syncHover).toBe(true) + expect(result.current.syncTooltip).toBe(true) expect(result.current.hoveredChart).toBe(null) expect(result.current.isHovered).toBe(false) expect(result.current.isCurrentChart).toBe(false) @@ -76,8 +76,8 @@ describe('useChartHoverState', () => { const { result } = renderHook(() => useChartHoverStateWithCorrupted('chart1')) - expect(result.current.syncHover).toBe(false) - expect(result.current.syncTooltip).toBe(false) + expect(result.current.syncHover).toBe(true) + expect(result.current.syncTooltip).toBe(true) expect(consoleWarnSpy).toHaveBeenCalled() }) }) @@ -301,7 +301,7 @@ describe('useChartHoverState', () => { const initialSyncHover = result.current.syncHover act(() => { - result.current.setSyncHover(false) + result.current.setSyncHover(initialSyncHover) }) expect(result.current.syncHover).toBe(initialSyncHover) diff --git a/apps/studio/components/ui/Charts/useChartHoverState.tsx b/apps/studio/components/ui/Charts/useChartHoverState.tsx index 0bbb2b6160f2e..672706d4a5abe 100644 --- a/apps/studio/components/ui/Charts/useChartHoverState.tsx +++ b/apps/studio/components/ui/Charts/useChartHoverState.tsx @@ -14,8 +14,8 @@ const CHART_TOOLTIP_SYNC_STORAGE_KEY = 'supabase-chart-tooltip-sync-enabled' let globalState: ChartHoverState = { hoveredIndex: null, hoveredChart: null, - syncHover: false, - syncTooltip: false, + syncHover: true, + syncTooltip: true, } // Subscribers for state changes diff --git a/apps/studio/data/reports/v2/edge-functions.config.ts b/apps/studio/data/reports/v2/edge-functions.config.ts index d5e824b6c09c4..64d7307f1ce46 100644 --- a/apps/studio/data/reports/v2/edge-functions.config.ts +++ b/apps/studio/data/reports/v2/edge-functions.config.ts @@ -16,22 +16,17 @@ import { NumericFilter } from 'components/interfaces/Reports/v2/ReportsNumericFi import { SelectFilters } from 'components/interfaces/Reports/v2/ReportsSelectFilter' type EdgeFunctionReportFilters = { - status_code?: NumericFilter - region?: SelectFilters - execution_time?: NumericFilter - functions?: SelectFilters + status_code: NumericFilter | null + region: SelectFilters + execution_time: NumericFilter | null + functions: SelectFilters } export function filterToWhereClause(filters?: EdgeFunctionReportFilters): string { const whereClauses: string[] = [] - if (filters?.functions) { - const selectedFunctions = Object.keys(filters.functions).filter( - (key) => filters.functions![key] - ) - if (selectedFunctions.length > 0) { - whereClauses.push(`function_id IN (${selectedFunctions.map((id) => `'${id}'`).join(',')})`) - } + if (filters?.functions && filters.functions.length > 0) { + whereClauses.push(`function_id IN (${filters.functions.map((id) => `'${id}'`).join(',')})`) } if (filters?.status_code) { @@ -40,13 +35,10 @@ export function filterToWhereClause(filters?: EdgeFunctionReportFilters): string ) } - if (filters?.region) { - const selectedRegions = Object.keys(filters.region).filter((key) => filters.region![key]) - if (selectedRegions.length > 0) { - whereClauses.push( - `h.x_sb_edge_region IN (${selectedRegions.map((region) => `'${region}'`).join(',')})` - ) - } + if (filters?.region && filters.region.length > 0) { + whereClauses.push( + `h.x_sb_edge_region IN (${filters.region.map((region) => `'${region}'`).join(',')})` + ) } if (filters?.execution_time) { diff --git a/apps/studio/data/reports/v2/edge-functions.test.tsx b/apps/studio/data/reports/v2/edge-functions.test.tsx index 6b790dd4a78f7..d02f656d39c4c 100644 --- a/apps/studio/data/reports/v2/edge-functions.test.tsx +++ b/apps/studio/data/reports/v2/edge-functions.test.tsx @@ -8,6 +8,13 @@ import { filterToWhereClause, } from './edge-functions.config' +const defaultFilters = { + status_code: null, + region: [], + execution_time: null, + functions: [], +} + describe('extractStatusCodesFromData', () => { it('should extract and sort unique status codes from the data', () => { const data = [ @@ -258,17 +265,14 @@ describe('filterToWhereClause', () => { }) it('should return empty string when filters object is empty', () => { - const result = filterToWhereClause({}) + const result = filterToWhereClause(defaultFilters) expect(result).toBe('') }) it('should generate WHERE clause for functions filter', () => { const filters = { - functions: { - func1: true, - func2: true, - func3: false, - }, + ...defaultFilters, + functions: ['func1', 'func2'], } const result = filterToWhereClause(filters) expect(result).toBe("WHERE function_id IN ('func1','func2')") @@ -276,6 +280,7 @@ describe('filterToWhereClause', () => { it('should generate WHERE clause for status_code filter', () => { const filters = { + ...defaultFilters, status_code: { operator: '>=' as const, value: 400, @@ -287,11 +292,8 @@ describe('filterToWhereClause', () => { it('should generate WHERE clause for region filter', () => { const filters = { - region: { - 'us-east-1': true, - 'eu-west-1': true, - 'ap-southeast-1': false, - }, + ...defaultFilters, + region: ['us-east-1', 'eu-west-1'], } const result = filterToWhereClause(filters) expect(result).toBe("WHERE h.x_sb_edge_region IN ('us-east-1','eu-west-1')") @@ -299,6 +301,7 @@ describe('filterToWhereClause', () => { it('should generate WHERE clause for execution_time filter', () => { const filters = { + ...defaultFilters, execution_time: { operator: '<' as const, value: 1000, @@ -310,17 +313,12 @@ describe('filterToWhereClause', () => { it('should combine multiple filters with AND', () => { const filters = { - functions: { - func1: true, - func2: false, - }, + functions: ['func1'], status_code: { operator: '=' as const, value: 200, }, - region: { - 'us-east-1': true, - }, + region: ['us-east-1'], execution_time: { operator: '<=' as const, value: 500, @@ -334,10 +332,8 @@ describe('filterToWhereClause', () => { it('should handle functions filter with no selected functions', () => { const filters = { - functions: { - func1: false, - func2: false, - }, + ...defaultFilters, + functions: [], } const result = filterToWhereClause(filters) expect(result).toBe('') @@ -345,10 +341,8 @@ describe('filterToWhereClause', () => { it('should handle region filter with no selected regions', () => { const filters = { - region: { - 'us-east-1': false, - 'eu-west-1': false, - }, + ...defaultFilters, + region: [], } const result = filterToWhereClause(filters) expect(result).toBe('') @@ -356,9 +350,8 @@ describe('filterToWhereClause', () => { it('should handle single function selection', () => { const filters = { - functions: { - 'single-func': true, - }, + ...defaultFilters, + functions: ['single-func'], } const result = filterToWhereClause(filters) expect(result).toBe("WHERE function_id IN ('single-func')") @@ -366,9 +359,8 @@ describe('filterToWhereClause', () => { it('should handle single region selection', () => { const filters = { - region: { - 'single-region': true, - }, + ...defaultFilters, + region: ['single-region'], } const result = filterToWhereClause(filters) expect(result).toBe("WHERE h.x_sb_edge_region IN ('single-region')") @@ -379,6 +371,7 @@ describe('filterToWhereClause', () => { operators.forEach((operator) => { const filters = { + ...defaultFilters, status_code: { operator, value: 200, @@ -394,6 +387,7 @@ describe('filterToWhereClause', () => { operators.forEach((operator) => { const filters = { + ...defaultFilters, execution_time: { operator, value: 100, @@ -406,6 +400,7 @@ describe('filterToWhereClause', () => { it('should handle numeric values correctly', () => { const filters = { + ...defaultFilters, status_code: { operator: '>' as const, value: 0, @@ -421,15 +416,9 @@ describe('filterToWhereClause', () => { it('should handle special characters in function IDs and regions', () => { const filters = { - functions: { - 'func-with-dash': true, - func_with_underscore: true, - 'func.with.dots': true, - }, - region: { - 'region-with-dash': true, - region_with_underscore: true, - }, + ...defaultFilters, + functions: ['func-with-dash', 'func_with_underscore', 'func.with.dots'], + region: ['region-with-dash', 'region_with_underscore'], } const result = filterToWhereClause(filters) expect(result).toBe( 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' }, + } + ) + } +} diff --git a/apps/studio/pages/project/[ref]/reports/edge-functions.tsx b/apps/studio/pages/project/[ref]/reports/edge-functions.tsx index 3b1b577741a7d..3cbcc7e27b26f 100644 --- a/apps/studio/pages/project/[ref]/reports/edge-functions.tsx +++ b/apps/studio/pages/project/[ref]/reports/edge-functions.tsx @@ -10,11 +10,11 @@ import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav' import { ReportChartV2 } from 'components/interfaces/Reports/v2/ReportChartV2' import { ReportsNumericFilter, - type NumericFilter, + numericFilterSchema, } from 'components/interfaces/Reports/v2/ReportsNumericFilter' import { ReportsSelectFilter, - type SelectFilters, + selectFilterSchema, } from 'components/interfaces/Reports/v2/ReportsSelectFilter' import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers' import DefaultLayout from 'components/layouts/DefaultLayout' @@ -33,6 +33,7 @@ import { EDGE_FUNCTION_REGIONS } from 'components/interfaces/Reports/Reports.con import { ReportSettings } from 'components/ui/Charts/ReportSettings' import { BASE_PATH } from 'lib/constants' import type { NextPageWithLayout } from 'types' +import { useQueryState, parseAsJson } from 'nuqs' const EdgeFunctionsReportV2: NextPageWithLayout = () => { return ( @@ -60,10 +61,24 @@ const EdgeFunctionsUsage = () => { useChartHoverState(chartSyncId) // Filters - const [statusCodeFilter, setStatusCodeFilter] = useState() - const [regionFilter, setRegionFilter] = useState({}) - const [executionTimeFilter, setExecutionTimeFilter] = useState() - const [functionFilter, setFunctionFilter] = useState({}) + const [statusCodeFilter, setStatusCodeFilter] = useQueryState( + 'status_code', + parseAsJson(numericFilterSchema.parse) + ) + + const [regionFilter, setRegionFilter] = useQueryState( + 'region', + parseAsJson(selectFilterSchema.parse) + ) + const [executionTimeFilter, setExecutionTimeFilter] = useQueryState( + 'execution_time', + parseAsJson(numericFilterSchema.parse) + ) + + const [functionFilter, setFunctionFilter] = useQueryState( + 'functions', + parseAsJson(selectFilterSchema.parse) + ) const { selectedDateRange, @@ -86,9 +101,9 @@ const EdgeFunctionsUsage = () => { endDate: selectedDateRange?.period_end?.date ?? '', interval: selectedDateRange?.interval ?? 'minute', filters: { - functions: functionFilter, + functions: functionFilter ?? [], status_code: statusCodeFilter, - region: regionFilter, + region: regionFilter ?? [], execution_time: executionTimeFilter, }, }) @@ -162,12 +177,13 @@ const EdgeFunctionsUsage = () => { options={ functions?.map((fn: { name: string; id: string }) => ({ label: fn.name, - key: fn.id, + value: fn.id, })) ?? [] } - value={functionFilter} + value={functionFilter ?? []} onChange={setFunctionFilter} isLoading={isRefreshing} + showSearch /> { ({ - key: region.key, + value: region.key, label: ( { ), }))} - value={regionFilter} + value={regionFilter ?? []} onChange={setRegionFilter} + showSearch /> } > - + {selectedDateRange && reportConfig .filter((report) => !report.hide)