From 2fbd0668f805192d96a562d36e471f5788711aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 15 Oct 2025 19:16:15 +0800 Subject: [PATCH 1/4] perf: remove customer endpoint call to determine missing address (#39549) Backend now exposes flag (waiting for prod deployment) --- .../hooks/misc/useOrganizationRestrictions.ts | 11 +---------- apps/studio/tests/helpers.tsx | 1 + packages/api-types/types/platform.d.ts | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/apps/studio/hooks/misc/useOrganizationRestrictions.ts b/apps/studio/hooks/misc/useOrganizationRestrictions.ts index 1486275bf510e..54bdd88a012bc 100644 --- a/apps/studio/hooks/misc/useOrganizationRestrictions.ts +++ b/apps/studio/hooks/misc/useOrganizationRestrictions.ts @@ -4,8 +4,6 @@ import { RESTRICTION_MESSAGES } from 'components/interfaces/Organization/restric import { useOverdueInvoicesQuery } from 'data/invoices/invoices-overdue-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { useBillingCustomerDataForm } from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm' -import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' import { useIsFeatureEnabled } from './useIsFeatureEnabled' export type WarningBannerProps = { @@ -20,7 +18,6 @@ export function useOrganizationRestrictions() { const { data: overdueInvoices } = useOverdueInvoicesQuery() const { data: organizations } = useOrganizationsQuery() - const { data: billingCustomer } = useOrganizationCustomerProfileQuery({ slug: org?.slug }) const warnings: WarningBannerProps[] = [] @@ -36,13 +33,7 @@ export function useOrganizationRestrictions() { (invoice) => invoice.organization_id === org?.id ) - if ( - org && - org.plan.id !== 'free' && - billingCustomer && - !billingCustomer.address?.line1 && - !org.billing_partner - ) { + if (org && org.organization_missing_address && !org.billing_partner) { warnings.push({ type: 'danger', title: RESTRICTION_MESSAGES.MISSING_BILLING_INFO.title, diff --git a/apps/studio/tests/helpers.tsx b/apps/studio/tests/helpers.tsx index 2cfef6e0b9a49..64dfcfcc5fc26 100644 --- a/apps/studio/tests/helpers.tsx +++ b/apps/studio/tests/helpers.tsx @@ -63,6 +63,7 @@ export const createMockOrganization = (details: Partial): Organiza opt_in_tags: [], restriction_status: null, restriction_data: null, + organization_missing_address: false, } return Object.assign(base, details) diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index b2a53293795be..fdfe2f256116b 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -5013,6 +5013,7 @@ export interface components { is_owner: boolean name: string opt_in_tags: string[] + organization_missing_address: boolean organization_requires_mfa: boolean plan: { /** @enum {string} */ @@ -7259,6 +7260,7 @@ export interface components { is_owner: boolean name: string opt_in_tags: string[] + organization_missing_address: boolean organization_requires_mfa: boolean plan: { /** @enum {string} */ @@ -8238,8 +8240,14 @@ export interface components { max_events_per_second: number | null /** @description Sets maximum number of joins per second rate limit */ max_joins_per_second: number | null + /** @description Sets maximum number of payload size in KB rate limit */ + max_payload_size_in_kb: number | null + /** @description Sets maximum number of presence events per second rate limit */ + max_presence_events_per_second: number | null /** @description Whether to only allow private channels */ private_only: boolean | null + /** @description Whether to suspend realtime */ + suspend: boolean | null } RegionsInfo: { all: { @@ -9989,8 +9997,14 @@ export interface components { max_events_per_second?: number /** @description Sets maximum number of joins per second rate limit */ max_joins_per_second?: number + /** @description Sets maximum number of payload size in KB rate limit */ + max_payload_size_in_kb?: number + /** @description Sets maximum number of presence events per second rate limit */ + max_presence_events_per_second?: number /** @description Whether to only allow private channels */ private_only?: boolean + /** @description Whether to suspend realtime */ + suspend?: boolean } UpdateReplicationDestinationBody: { /** @description Destination configuration */ From 70a64f8c00b45370d7fd73fb7803494e953bfaf9 Mon Sep 17 00:00:00 2001 From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:39:29 +0100 Subject: [PATCH 2/4] feat(studio): query performance metrics chart (#39431) * feat: setup chart area and tabs This sets up the area where we can expect the insights chart as well as the tabs mechanism. * feat: parse pg_stat_monitor logs as json * feat: create query perf chart utils and move transfrom function Created a utils file for our QueryPerformanceChart component. This moves the logs to JSON transform function there. * feat: add timerange to chart * feat: add date selector to query perf overview This adds the selector to the top right of the page allowing the user to switch between last hour, 3 hours and 24 hours * feat: modify chart component to accomodate hiding bits * feat: add metrics to each tab * chore: update to 60 min by default and some css * feat: centralise data parsing for logs * feat: clean up filters bar This rewires the export to give you the aggregate pg_stat_monitor data. Also removes unused buttons and filters. * feat: percentiles for query latency chart * feat: filter out non evenets from pg_stat_monitor logs * feat: utils for cache misses and hits * feat: add selected query to chart on click * feat: add click through to query panel * chore: tidy up files * chore: distinction between selected and open panel * feat: move query performance fully into reports area * fix: preserve query params on reports link * fix: remove right icon syntax in report menu * chore: remove cache misses from cache chart * refactor: backwards compatibility for statements if right db version isnt available * chore: delete randomly generated empty file * chore: tidy up unused imports and vars * chore: remove console logs * chore: remove isMounted from query perf * fix: cmd k query perf path * feat: simplify query latency only p50 and p95 This seems to give us a more accurate reading as we can calculate these two * fix: cache hit rate not showing inside query details * chore: chart bg colour adjust So it contrasts a little better on light mode. * feat: show selected query on other verticals * feat: bring back symlink in advisors --- .../EnableIndexAdvisorButton.tsx | 0 .../IndexAdvisorDisabledState.tsx | 2 +- .../IndexImprovementText.tsx | 0 .../IndexSuggestionIcon.tsx | 4 +- .../{ => IndexAdvisor}/index-advisor.utils.ts | 2 +- .../QueryPerformance/QueryDetail.tsx | 6 +- .../QueryPerformance/QueryIndexes.tsx | 10 +- .../QueryPerformance.constants.ts | 63 +++ .../QueryPerformance/QueryPerformance.tsx | 154 ++------ .../QueryPerformance.types.ts | 18 + .../QueryPerformance.utils.ts | 11 + .../QueryPerformanceChart.tsx | 360 ++++++++++++++++++ .../QueryPerformanceFilterBar.tsx | 86 +---- .../QueryPerformance/QueryPerformanceGrid.tsx | 157 ++++---- .../QueryPerformance/TextSearchPopover.tsx | 81 ---- .../WithMonitor/WithMonitor.tsx | 111 ++++++ .../WithMonitor/WithMonitor.utils.ts | 301 +++++++++++++++ .../WithStatements/WithStatements.tsx | 186 +++++++++ .../WithStatements/WithStatements.utils.ts | 22 ++ .../hooks/useIsIndexAdvisorStatus.ts | 2 +- ...rsMenu.utils.ts => AdvisorsMenu.utils.tsx} | 4 +- .../ReportsLayout/Reports.Commands.tsx | 2 +- .../layouts/ReportsLayout/ReportsMenu.tsx | 8 +- .../components/ui/Charts/ChartHeader.tsx | 21 +- .../components/ui/Charts/ComposedChart.tsx | 6 + .../query-performance.tsx | 40 +- 26 files changed, 1269 insertions(+), 388 deletions(-) rename apps/studio/components/interfaces/QueryPerformance/{ => IndexAdvisor}/EnableIndexAdvisorButton.tsx (100%) rename apps/studio/components/interfaces/QueryPerformance/{ => IndexAdvisor}/IndexAdvisorDisabledState.tsx (98%) rename apps/studio/components/interfaces/QueryPerformance/{ => IndexAdvisor}/IndexImprovementText.tsx (100%) rename apps/studio/components/interfaces/QueryPerformance/{ => IndexAdvisor}/IndexSuggestionIcon.tsx (97%) rename apps/studio/components/interfaces/QueryPerformance/{ => IndexAdvisor}/index-advisor.utils.ts (97%) create mode 100644 apps/studio/components/interfaces/QueryPerformance/QueryPerformance.types.ts create mode 100644 apps/studio/components/interfaces/QueryPerformance/QueryPerformanceChart.tsx delete mode 100644 apps/studio/components/interfaces/QueryPerformance/TextSearchPopover.tsx create mode 100644 apps/studio/components/interfaces/QueryPerformance/WithMonitor/WithMonitor.tsx create mode 100644 apps/studio/components/interfaces/QueryPerformance/WithMonitor/WithMonitor.utils.ts create mode 100644 apps/studio/components/interfaces/QueryPerformance/WithStatements/WithStatements.tsx create mode 100644 apps/studio/components/interfaces/QueryPerformance/WithStatements/WithStatements.utils.ts rename apps/studio/components/layouts/AdvisorsLayout/{AdvisorsMenu.utils.ts => AdvisorsMenu.utils.tsx} (86%) rename apps/studio/pages/project/[ref]/{advisors => reports}/query-performance.tsx (61%) diff --git a/apps/studio/components/interfaces/QueryPerformance/EnableIndexAdvisorButton.tsx b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton.tsx similarity index 100% rename from apps/studio/components/interfaces/QueryPerformance/EnableIndexAdvisorButton.tsx rename to apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton.tsx diff --git a/apps/studio/components/interfaces/QueryPerformance/IndexAdvisorDisabledState.tsx b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorDisabledState.tsx similarity index 98% rename from apps/studio/components/interfaces/QueryPerformance/IndexAdvisorDisabledState.tsx rename to apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorDisabledState.tsx index 18e8be8ae945f..be9fdbe3a5a56 100644 --- a/apps/studio/components/interfaces/QueryPerformance/IndexAdvisorDisabledState.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorDisabledState.tsx @@ -8,7 +8,7 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' -import { Markdown } from '../Markdown' +import { Markdown } from '../../Markdown' import { getIndexAdvisorExtensions } from './index-advisor.utils' export const IndexAdvisorDisabledState = () => { diff --git a/apps/studio/components/interfaces/QueryPerformance/IndexImprovementText.tsx b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexImprovementText.tsx similarity index 100% rename from apps/studio/components/interfaces/QueryPerformance/IndexImprovementText.tsx rename to apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexImprovementText.tsx diff --git a/apps/studio/components/interfaces/QueryPerformance/IndexSuggestionIcon.tsx b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexSuggestionIcon.tsx similarity index 97% rename from apps/studio/components/interfaces/QueryPerformance/IndexSuggestionIcon.tsx rename to apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexSuggestionIcon.tsx index 7ecc597535998..5aed88b1479b1 100644 --- a/apps/studio/components/interfaces/QueryPerformance/IndexSuggestionIcon.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexSuggestionIcon.tsx @@ -14,8 +14,8 @@ import { WarningIcon, } from 'ui' import { IndexImprovementText } from './IndexImprovementText' -import { QueryPanelScoreSection } from './QueryPanel' -import { useIndexInvalidation } from './hooks/useIndexInvalidation' +import { QueryPanelScoreSection } from '../QueryPanel' +import { useIndexInvalidation } from '../hooks/useIndexInvalidation' import { createIndexes } from './index-advisor.utils' interface IndexSuggestionIconProps { diff --git a/apps/studio/components/interfaces/QueryPerformance/index-advisor.utils.ts b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts similarity index 97% rename from apps/studio/components/interfaces/QueryPerformance/index-advisor.utils.ts rename to apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts index b6275421cbd37..134aea1c3226c 100644 --- a/apps/studio/components/interfaces/QueryPerformance/index-advisor.utils.ts +++ b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts @@ -96,7 +96,7 @@ export async function createIndexes({ * @returns Whether there are index recommendations available */ export function hasIndexRecommendations( - result: GetIndexAdvisorResultResponse | undefined, + result: GetIndexAdvisorResultResponse | undefined | null, isSuccess: boolean ): boolean { return Boolean(isSuccess && result?.index_statements && result.index_statements.length > 0) diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx index f35e1e9e30f3c..6234374fc9874 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx @@ -1,11 +1,9 @@ -import { Lightbulb, ChevronsUpDown, Expand } from 'lucide-react' +import { Lightbulb, ChevronsUpDown } from 'lucide-react' import { useEffect, useState } from 'react' import dynamic from 'next/dynamic' -import dayjs from 'dayjs' import { formatSql } from 'lib/formatSql' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { QueryPanelContainer, QueryPanelSection } from './QueryPanel' import { QUERY_PERFORMANCE_COLUMNS, @@ -170,7 +168,7 @@ export const QueryDetail = ({ selectedRow, onClickViewSuggestion }: QueryDetailP return (
  • {x.name}

    - {typeof rawValue === 'string' ? ( + {typeof rawValue === 'string' || typeof rawValue === 'number' ? (

    + ` +select + id, + pgl.timestamp as timestamp, + 'postgres' as log_type, + CAST(pgl_parsed.sql_state_code AS STRING) as status, + CASE + WHEN pgl_parsed.error_severity = 'LOG' THEN 'success' + WHEN pgl_parsed.error_severity = 'WARNING' THEN 'warning' + WHEN pgl_parsed.error_severity = 'FATAL' THEN 'error' + WHEN pgl_parsed.error_severity = 'ERROR' THEN 'error' + ELSE null + END as level, + event_message as event_message +from postgres_logs as pgl +cross join unnest(pgl.metadata) as pgl_metadata +cross join unnest(pgl_metadata.parsed) as pgl_parsed +WHERE pgl.event_message LIKE '%[pg_stat_monitor]%' + AND pgl.timestamp >= CAST('${startTime}' AS TIMESTAMP) + AND pgl.timestamp <= CAST('${endTime}' AS TIMESTAMP) +ORDER BY timestamp DESC +`.trim() + +export const PG_STAT_MONITOR_LOGS_QUERY = getPgStatMonitorLogsQuery( + new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + new Date().toISOString() +) diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx index d57961890d567..82844f1ca1900 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx @@ -1,158 +1,50 @@ -import { X } from 'lucide-react' -import { useEffect, useState } from 'react' -import { toast } from 'sonner' +import { useEffect } from 'react' -import { LOCAL_STORAGE_KEYS, useParams } from 'common' -import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' -import { formatDatabaseID } from 'data/read-replicas/replicas.utils' -import { executeSql } from 'data/sql/execute-sql-query' +import { WithMonitor } from './WithMonitor/WithMonitor' +import { WithStatements } from './WithStatements/WithStatements' +import { useParams } from 'common' import { DbQueryHook } from 'hooks/analytics/useDbQuery' -import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { DOCS_URL, IS_PLATFORM } from 'lib/constants' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' -import { Button, LoadingLine, cn } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { Markdown } from '../Markdown' import { PresetHookResult } from '../Reports/Reports.utils' -import { QueryPerformanceFilterBar } from './QueryPerformanceFilterBar' -import { QueryPerformanceGrid } from './QueryPerformanceGrid' -import { QueryPerformanceMetrics } from './QueryPerformanceMetrics' interface QueryPerformanceProps { queryHitRate: PresetHookResult queryPerformanceQuery: DbQueryHook queryMetrics: PresetHookResult + isPgStatMonitorEnabled: boolean + dateRange?: { + period_start: { date: string; time_period: string } + period_end: { date: string; time_period: string } + interval: string + } + onDateRangeChange?: (from: string, to: string) => void } export const QueryPerformance = ({ queryHitRate, queryPerformanceQuery, queryMetrics, + isPgStatMonitorEnabled, + dateRange, + onDateRangeChange, }: QueryPerformanceProps) => { const { ref } = useParams() - const { data: project } = useSelectedProjectQuery() const state = useDatabaseSelectorStateSnapshot() - const { isLoading, isRefetching } = queryPerformanceQuery - const isPrimaryDatabase = state.selectedDatabaseId === ref - const formattedDatabaseId = formatDatabaseID(state.selectedDatabaseId ?? '') - - const [showResetgPgStatStatements, setShowResetgPgStatStatements] = useState(false) - - const [showBottomSection, setShowBottomSection] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.QUERY_PERF_SHOW_BOTTOM_SECTION, - true - ) - - const handleRefresh = () => { - queryPerformanceQuery.runQuery() - queryHitRate.runQuery() - queryMetrics.runQuery() - } - - const { data: databases } = useReadReplicasQuery({ projectRef: ref }) - useEffect(() => { state.setSelectedDatabaseId(ref) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ref]) - return ( - <> - - setShowResetgPgStatStatements(true)} - /> - - - - -

    - -
    -

    Reset report

    -

    - Consider resetting the analysis after optimizing any queries -

    - -
    - -
    -

    How is this report generated?

    - -
    - -
    -

    Inspect your database for potential issues

    - -
    -
    - - setShowResetgPgStatStatements(false)} - onConfirm={async () => { - const connectionString = databases?.find( - (db) => db.identifier === state.selectedDatabaseId - )?.connectionString - - if (IS_PLATFORM && !connectionString) { - return toast.error('Unable to run query: Connection string is missing') - } + if (isPgStatMonitorEnabled) { + return + } - try { - await executeSql({ - projectRef: project?.ref, - connectionString, - sql: `SELECT pg_stat_statements_reset();`, - }) - handleRefresh() - setShowResetgPgStatStatements(false) - } catch (error: any) { - toast.error(`Failed to reset analysis: ${error.message}`) - } - }} - > -

    - This will reset the pg_stat_statements table in the extensions schema on your{' '} - - {isPrimaryDatabase ? 'primary database' : `read replica (ID: ${formattedDatabaseId})`} - - , which is used to calculate query performance. This data will repopulate immediately - after. -

    -
    - + return ( + ) } diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.types.ts b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.types.ts new file mode 100644 index 0000000000000..e7499b46d9a9f --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.types.ts @@ -0,0 +1,18 @@ +import { GetIndexAdvisorResultResponse } from 'data/database/retrieve-index-advisor-result-query' + +export interface QueryPerformanceRow { + query: string + prop_total_time: number + total_time: number + calls: number + max_time: number + mean_time: number + min_time: number + rows_read: number + cache_hit_rate: number + rolname: string + index_advisor_result?: GetIndexAdvisorResultResponse | null + _total_cache_hits?: number + _total_cache_misses?: number + _count?: number +} diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.utils.ts b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.utils.ts index ce7d211123e92..fb13634e4b244 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.utils.ts +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.utils.ts @@ -24,3 +24,14 @@ export const formatDuration = (milliseconds: number) => { return parts.length > 0 ? parts.join(' ') : '0s' } + +export const transformLogsToJSON = (log: string) => { + try { + let jsonString = log.replace('[pg_stat_monitor] ', '') + jsonString = jsonString.replace(/""/g, '","') + const jsonObject = JSON.parse(jsonString) + return jsonObject + } catch (error) { + return null + } +} diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceChart.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceChart.tsx new file mode 100644 index 0000000000000..30fa2578e1f65 --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceChart.tsx @@ -0,0 +1,360 @@ +import { useState, useMemo } from 'react' +import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' +import { QUERY_PERFORMANCE_CHART_TABS } from './QueryPerformance.constants' +import { Loader2 } from 'lucide-react' +import { ComposedChart } from 'components/ui/Charts/ComposedChart' +import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils' +import type { ChartDataPoint } from './WithMonitor/WithMonitor.utils' + +interface QueryPerformanceChartProps { + dateRange?: { + period_start: { date: string; time_period: string } + period_end: { date: string; time_period: string } + interval: string + } + onDateRangeChange?: (from: string, to: string) => void + chartData: ChartDataPoint[] + isLoading: boolean + error: any + currentSelectedQuery: string | null + parsedLogs: any[] +} + +const QueryMetricBlock = ({ + label, + value, +}: { + label: string + value: string | number | undefined +}) => { + return ( +
    + {label} + {value} +
    + ) +} + +const formatTimeValue = (value: number): string => { + if (value >= 1000) { + return `${(value / 1000).toFixed(1)}s` + } + return `${value.toFixed(1)}ms` +} + +const formatNumberValue = (value: number): string => { + return value.toLocaleString() +} + +export const QueryPerformanceChart = ({ + onDateRangeChange, + chartData, + isLoading, + error, + currentSelectedQuery, + parsedLogs, +}: QueryPerformanceChartProps) => { + const [selectedMetric, setSelectedMetric] = useState('query_latency') + + const currentMetrics = useMemo(() => { + if (!chartData || chartData.length === 0) return [] + + switch (selectedMetric) { + case 'query_latency': { + const avgP95 = chartData.reduce((sum, d) => sum + d.p95_time, 0) / chartData.length + + return [ + { + label: 'Average p95', + value: avgP95 >= 100 ? `${(avgP95 / 1000).toFixed(2)}s` : `${Math.round(avgP95)}ms`, + }, + ] + } + case 'rows_read': { + const totalRowsRead = chartData.reduce((sum, d) => sum + d.rows_read, 0) + + return [ + { + label: 'Total Rows Read', + value: totalRowsRead.toLocaleString(), + }, + ] + } + case 'calls': { + const totalCalls = chartData.reduce((sum, d) => sum + d.calls, 0) + + return [ + { + label: 'Total Calls', + value: totalCalls.toLocaleString(), + }, + ] + } + case 'cache_hits': { + const totalHits = chartData.reduce((sum, d) => sum + d.cache_hits, 0) + const totalMisses = chartData.reduce((sum, d) => sum + d.cache_misses, 0) + const total = totalHits + totalMisses + const hitRate = total > 0 ? (totalHits / total) * 100 : 0 + + return [ + { + label: 'Cache Hit Rate', + value: `${hitRate.toFixed(2)}%`, + }, + ] + } + default: + return [] + } + }, [chartData, selectedMetric]) + + const transformedChartData = useMemo(() => { + if (selectedMetric !== 'query_latency') return chartData + + const transformed = chartData.map((dataPoint) => ({ + ...dataPoint, + p50_time: parseFloat((dataPoint.p50_time / 1000).toFixed(3)), + p95_time: parseFloat((dataPoint.p95_time / 1000).toFixed(3)), + })) + + return transformed + }, [chartData, selectedMetric]) + + const querySpecificData = useMemo(() => { + if (!currentSelectedQuery || !parsedLogs.length) return null + + const normalizedSelected = currentSelectedQuery.replace(/\s+/g, ' ').trim() + const queryLogs = parsedLogs.filter((log) => { + const normalized = (log.query || '').replace(/\s+/g, ' ').trim() + return normalized === normalizedSelected + }) + + const queryDataMap = new Map< + number, + { + time: number + rows_read: number + calls: number + cache_hits: number + } + >() + + queryLogs.forEach((log) => { + const timestamps = [log.bucket_start_time, log.bucket, log.timestamp, log.ts] + const validTimestamp = timestamps.find((t) => t && !isNaN(new Date(t).getTime())) + + if (!validTimestamp) return + + const time = new Date(validTimestamp).getTime() + const meanTime = log.mean_time ?? log.mean_exec_time ?? log.mean_query_time ?? 0 + const rowsRead = log.rows_read ?? log.rows ?? 0 + const calls = log.calls ?? 0 + const cacheHits = log.shared_blks_hit ?? log.cache_hits ?? 0 + + queryDataMap.set(time, { + time: parseFloat(String(meanTime)), + rows_read: parseFloat(String(rowsRead)), + calls: parseFloat(String(calls)), + cache_hits: parseFloat(String(cacheHits)), + }) + }) + + return queryDataMap + }, [currentSelectedQuery, parsedLogs]) + + const mergedChartData = useMemo(() => { + if (!querySpecificData || !currentSelectedQuery) { + return transformedChartData + } + + return transformedChartData.map((dataPoint) => { + const queryData = querySpecificData.get(dataPoint.period_start) + + return { + ...dataPoint, + selected_query_time: + queryData?.time !== undefined + ? selectedMetric === 'query_latency' + ? queryData.time / 1000 + : queryData.time + : null, + selected_query_rows_read: queryData?.rows_read !== undefined ? queryData.rows_read : null, + selected_query_calls: queryData?.calls !== undefined ? queryData.calls : null, + selected_query_cache_hits: + queryData?.cache_hits !== undefined ? queryData.cache_hits : null, + } + }) + }, [transformedChartData, querySpecificData, currentSelectedQuery, selectedMetric]) + + const getChartAttributes = useMemo((): MultiAttribute[] => { + const attributeMap: Record = { + query_latency: [ + { + attribute: 'p50_time', + label: 'p50', + provider: 'logs', + type: 'line', + color: { light: '#8B5CF6', dark: '#8B5CF6' }, + }, + { + attribute: 'p95_time', + label: 'p95', + provider: 'logs', + type: 'line', + color: { light: '#65BCD9', dark: '#65BCD9' }, + }, + ], + rows_read: [ + { + attribute: 'rows_read', + label: 'Rows Read', + provider: 'logs', + }, + ], + calls: [ + { + attribute: 'calls', + label: 'Calls', + provider: 'logs', + }, + ], + cache_hits: [ + { + attribute: 'cache_hits', + label: 'Cache Hits', + provider: 'logs', + type: 'line', + color: { light: '#10B981', dark: '#10B981' }, + }, + ], + } + + const baseAttributes = attributeMap[selectedMetric] || [] + + // Add selected query line based on current metric + if (currentSelectedQuery && querySpecificData) { + const selectedQueryAttributes: Record = { + query_latency: { + attribute: 'selected_query_time', + label: 'Selected Query', + provider: 'logs', + type: 'line', + color: { light: '#10B981', dark: '#10B981' }, + strokeWidth: 3, + }, + rows_read: { + attribute: 'selected_query_rows_read', + label: 'Selected Query', + provider: 'logs', + type: 'line', + color: { light: '#F59E0B', dark: '#F59E0B' }, + strokeWidth: 3, + }, + calls: { + attribute: 'selected_query_calls', + label: 'Selected Query', + provider: 'logs', + type: 'line', + color: { light: '#EC4899', dark: '#EC4899' }, + strokeWidth: 3, + }, + cache_hits: { + attribute: 'selected_query_cache_hits', + label: 'Selected Query', + provider: 'logs', + type: 'line', + color: { light: '#8B5CF6', dark: '#8B5CF6' }, + strokeWidth: 3, + }, + } + + const selectedQueryAttr = selectedQueryAttributes[selectedMetric] + if (selectedQueryAttr) { + return [...baseAttributes, selectedQueryAttr] + } + } + + return baseAttributes + }, [selectedMetric, currentSelectedQuery, querySpecificData]) + + const updateDateRange = (from: string, to: string) => { + onDateRangeChange?.(from, to) + } + + const getYAxisFormatter = useMemo(() => { + if (selectedMetric === 'query_latency') { + return formatTimeValue + } + return formatNumberValue + }, [selectedMetric]) + + return ( +
    + setSelectedMetric(value as string)} + className="w-full" + > + + {QUERY_PERFORMANCE_CHART_TABS.map((tab) => ( + + {tab.label} + + ))} + + + +
    + {isLoading ? ( + + ) : error ? ( +

    + Error loading chart data +

    + ) : ( +
    +
    + {currentMetrics.map((metric, index) => ( + + ))} +
    + +
    + )} +
    +
    +
    +
    + ) +} diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx index a2d91081a21d1..9c115b62f843a 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx @@ -1,60 +1,23 @@ import { useDebounce } from '@uidotdev/usehooks' -import { RefreshCw, Search, X } from 'lucide-react' -import { parseAsArrayOf, parseAsString, useQueryStates } from 'nuqs' -import { ChangeEvent, useEffect, useState } from 'react' +import { Search, X } from 'lucide-react' +import { parseAsString, useQueryStates } from 'nuqs' +import { ChangeEvent, ReactNode, useEffect, useState } from 'react' -import { LOCAL_STORAGE_KEYS, useParams } from 'common' -import { DownloadResultsButton } from 'components/ui/DownloadResultsButton' -import { FilterPopover } from 'components/ui/FilterPopover' -import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' -import { DbQueryHook } from 'hooks/analytics/useDbQuery' -import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort' -export const QueryPerformanceFilterBar = ({ - queryPerformanceQuery, - onResetReportClick, -}: { - queryPerformanceQuery: DbQueryHook - onResetReportClick?: () => void -}) => { - const { ref } = useParams() - const { data: project } = useSelectedProjectQuery() +export const QueryPerformanceFilterBar = ({ actions }: { actions?: ReactNode }) => { const { sort, clearSort } = useQueryPerformanceSort() - const [showBottomSection] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.QUERY_PERF_SHOW_BOTTOM_SECTION, - true - ) - const [{ search: searchQuery, roles: defaultFilterRoles }, setSearchParams] = useQueryStates({ + const [{ search: searchQuery }, setSearchParams] = useQueryStates({ search: parseAsString.withDefault(''), - roles: parseAsArrayOf(parseAsString).withDefault([]), }) const [inputValue, setInputValue] = useState(searchQuery) const debouncedInputValue = useDebounce(inputValue, 500) const searchValue = inputValue.length === 0 ? inputValue : debouncedInputValue - const [filters, setFilters] = useState<{ roles: string[]; query: string }>({ - roles: defaultFilterRoles, - query: '', - }) - - const { isLoading, isRefetching } = queryPerformanceQuery - const { data, isLoading: isLoadingRoles } = useDatabaseRolesQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const roles = (data ?? []).sort((a, b) => a.name.localeCompare(b.name)) - - const onFilterRolesChange = (roles: string[]) => { - setFilters({ ...filters, roles }) - setSearchParams({ roles }) - } - const onSearchQueryChange = (value: string) => { setSearchParams({ search: value || '' }) } @@ -65,7 +28,7 @@ export const QueryPerformanceFilterBar = ({ }, [searchValue]) return ( -
    +
    - - {sort && (

    @@ -116,32 +69,7 @@ export const QueryPerformanceFilterBar = ({ )}

    - -
    - {!showBottomSection && onResetReportClick && ( - - )} - - -
    +
    {actions}
    ) } diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx index e8e3c87ee0b80..e680c93d09dba 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx @@ -1,9 +1,8 @@ -import { ArrowDown, ArrowUp, ChevronDown, TextSearch } from 'lucide-react' +import { ArrowDown, ArrowUp, ChevronDown, ArrowRight, TextSearch } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid' import { useParams } from 'common' -import { DbQueryHook } from 'hooks/analytics/useDbQuery' import { Button, DropdownMenu, @@ -22,8 +21,8 @@ import { } from 'ui' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' -import { hasIndexRecommendations } from './index-advisor.utils' -import { IndexSuggestionIcon } from './IndexSuggestionIcon' +import { hasIndexRecommendations } from './IndexAdvisor/index-advisor.utils' +import { IndexSuggestionIcon } from './IndexAdvisor/IndexSuggestionIcon' import { QueryDetail } from './QueryDetail' import { QueryIndexes } from './QueryIndexes' import { @@ -33,24 +32,14 @@ import { } from './QueryPerformance.constants' import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort' import { formatDuration } from './QueryPerformance.utils' -import { GetIndexAdvisorResultResponse } from 'data/database/retrieve-index-advisor-result-query' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { QueryPerformanceRow } from './QueryPerformance.types' interface QueryPerformanceGridProps { - queryPerformanceQuery: DbQueryHook -} - -interface QueryPerformanceRow { - query: string - prop_total_time: number - total_time: number - calls: number - max_time: number - mean_time: number - min_time: number - rows_read: number - cache_hit_rate: string - rolname: string - index_advisor_result: GetIndexAdvisorResultResponse | null + aggregatedData: QueryPerformanceRow[] + isLoading: boolean + currentSelectedQuery?: string | null // Make optional + onCurrentSelectQuery?: (query: string) => void // Make optional } const calculateTimeConsumedWidth = (data: QueryPerformanceRow[]) => { @@ -75,11 +64,15 @@ const calculateTimeConsumedWidth = (data: QueryPerformanceRow[]) => { return Math.min(maxWidth, 300) } -export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformanceGridProps) => { +export const QueryPerformanceGrid = ({ + aggregatedData, + isLoading, + currentSelectedQuery, + onCurrentSelectQuery, +}: QueryPerformanceGridProps) => { const { sort, setSortConfig } = useQueryPerformanceSort() const gridRef = useRef(null) const { sort: urlSort, order, roles, search } = useParams() - const { isLoading, data } = queryPerformanceQuery const dataGridContainerRef = useRef(null) const [view, setView] = useState<'details' | 'suggestion'>('details') @@ -95,7 +88,9 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance cellClass: `column-${col.id}`, resizable: true, minWidth: - col.id === 'prop_total_time' ? calculateTimeConsumedWidth(data ?? []) : col.minWidth ?? 120, + col.id === 'prop_total_time' + ? calculateTimeConsumedWidth((aggregatedData as any) ?? []) + : col.minWidth ?? 120, sortable: !nonSortableColumns.includes(col.id), headerCellClass: 'first:pl-6 cursor-pointer', renderHeaderCell: () => { @@ -156,26 +151,43 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance const value = props.row?.[col.id] if (col.id === 'query') { return ( -
    - {hasIndexRecommendations(props.row.index_advisor_result, true) && ( - { - setSelectedRow(props.rowIdx) - setView('suggestion') - gridRef.current?.scrollToCell({ idx: 0, rowIdx: props.rowIdx }) - }} - /> - )} +
    +
    + {hasIndexRecommendations(props.row.index_advisor_result, true) && ( + { + setSelectedRow(props.rowIdx) + setView('suggestion') + gridRef.current?.scrollToCell({ idx: 0, rowIdx: props.rowIdx }) + }} + /> + )} +
    + {onCurrentSelectQuery && ( + } + size="tiny" + type="default" + onClick={(e) => { + e.stopPropagation() + setSelectedRow(props.rowIdx) + setView('details') + gridRef.current?.scrollToCell({ idx: 0, rowIdx: props.rowIdx }) + }} + className="p-1 flex-shrink-0 -translate-x-2 group-hover:flex hidden" + /> + )}
    ) } @@ -267,21 +279,12 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance ) } - const cacheHitRateToNumber = (value: number | string) => { - if (typeof value === 'number') return value - return parseFloat(value.toString().replace('%', '')) || 0 - } - if (col.id === 'cache_hit_rate') { return (
    - {typeof value === 'string' ? ( -

    - {cacheHitRateToNumber(value).toLocaleString(undefined, { + {typeof value === 'number' && !isNaN(value) && isFinite(value) ? ( +

    + {value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} @@ -325,27 +328,36 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance }) const reportData = useMemo(() => { - const rawData = data ?? [] + let data = [...aggregatedData] - if (sort?.column === 'prop_total_time') { - const sortedData = [...rawData].sort((a, b) => { - const getNumericValue = (value: number | string) => { - if (!value || value === 'n/a') return 0 - if (typeof value === 'number') return value - return parseFloat(value.toString().replace('%', '')) || 0 - } + if (search && typeof search === 'string' && search.length > 0) { + data = data.filter((row) => row.query.toLowerCase().includes(search.toLowerCase())) + } - const aValue = getNumericValue(a.prop_total_time) - const bValue = getNumericValue(b.prop_total_time) + if (roles && Array.isArray(roles) && roles.length > 0) { + data = data.filter((row) => row.rolname && roles.includes(row.rolname)) + } + if (sort?.column === 'prop_total_time') { + data.sort((a, b) => { + const aValue = a.prop_total_time || 0 + const bValue = b.prop_total_time || 0 return sort.order === 'asc' ? aValue - bValue : bValue - aValue }) + } else if (sort?.column && sort.column !== 'query') { + data.sort((a, b) => { + const aValue = a[sort.column as keyof QueryPerformanceRow] || 0 + const bValue = b[sort.column as keyof QueryPerformanceRow] || 0 - return sortedData + if (typeof aValue === 'number' && typeof bValue === 'number') { + return sort.order === 'asc' ? aValue - bValue : bValue - aValue + } + return 0 + }) } - return rawData - }, [data, sort]) + return data + }, [aggregatedData, sort, search, roles]) const selectedQuery = selectedRow !== undefined ? reportData[selectedRow]?.query : undefined const query = (selectedQuery ?? '').trim().toLowerCase() @@ -366,7 +378,6 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return - // stop default RDG behavior (which moves focus to header when selectedRow is 0) event.stopPropagation() let nextIndex = selectedRow @@ -410,9 +421,14 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance rows={reportData} rowClass={(_, idx) => { const isSelected = idx === selectedRow + const query = reportData[idx]?.query + const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false + return [ `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`, - `${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-foreground' : ''}`, + `${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:!border-l-foreground' : ''}`, + `${isCharted ? 'bg-surface-200 dark:bg-surface-200' : ''}`, + `${isCharted ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-brand' : ''}`, '[&>.rdg-cell]:box-border [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', '[&>.rdg-cell.column-prop_total_time]:relative', ].join(' ') @@ -427,12 +443,17 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance event.stopPropagation() if (typeof idx === 'number' && idx >= 0) { - setSelectedRow(idx) - gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) - - const rowQuery = reportData[idx]?.query ?? '' - if (!rowQuery.trim().toLowerCase().startsWith('select')) { + // If onCurrentSelectQuery is provided, use the chart selection logic + if (onCurrentSelectQuery) { + const query = reportData[idx]?.query + if (query) { + onCurrentSelectQuery(query) + } + } else { + // Otherwise, open the detail panel + setSelectedRow(idx) setView('details') + gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) } } }} diff --git a/apps/studio/components/interfaces/QueryPerformance/TextSearchPopover.tsx b/apps/studio/components/interfaces/QueryPerformance/TextSearchPopover.tsx deleted file mode 100644 index 1e6be9f3c9ed6..0000000000000 --- a/apps/studio/components/interfaces/QueryPerformance/TextSearchPopover.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useState } from 'react' -import { - Button, - PopoverContent_Shadcn_, - PopoverTrigger_Shadcn_, - Popover_Shadcn_, - TextArea_Shadcn_, -} from 'ui' - -interface TextSearchPopoverProps { - name: string - value: string - placeholder?: string - rows?: number - onSaveText: (value: string) => void -} - -export const TextSearchPopover = ({ - name, - value = '', - placeholder, - rows = 4, - onSaveText, -}: TextSearchPopoverProps) => { - const [open, setOpen] = useState(false) - const [search, setSearch] = useState(value) - - const applySearch = () => { - onSaveText(search) - setOpen(false) - } - - return ( - - - - - -

    - setSearch(event.target.value)} - rows={rows} - className="text-xs font-mono tracking-tight" - placeholder={placeholder ?? 'Search for a query'} - onKeyDown={(event) => { - if (event.metaKey && (event.code === 'Enter' || event.code === 'NumpadEnter')) - applySearch() - }} - /> -
    -
    - - -
    - - - ) -} diff --git a/apps/studio/components/interfaces/QueryPerformance/WithMonitor/WithMonitor.tsx b/apps/studio/components/interfaces/QueryPerformance/WithMonitor/WithMonitor.tsx new file mode 100644 index 0000000000000..db34a8def4c6c --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformance/WithMonitor/WithMonitor.tsx @@ -0,0 +1,111 @@ +import { QueryPerformanceGrid } from '../QueryPerformanceGrid' +import { LoadingLine } from 'ui' +import { QueryPerformanceChart } from '../QueryPerformanceChart' +import { QueryPerformanceFilterBar } from '../QueryPerformanceFilterBar' +import { useMemo, useState } from 'react' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import useLogsQuery from 'hooks/analytics/useLogsQuery' +import { getPgStatMonitorLogsQuery } from '../QueryPerformance.constants' +import { + parsePgStatMonitorLogs, + transformLogsToChartData, + aggregateLogsByQuery, +} from './WithMonitor.utils' +import { useParams } from 'common' +import { DownloadResultsButton } from 'components/ui/DownloadResultsButton' + +dayjs.extend(utc) + +interface WithMonitorProps { + dateRange?: { + period_start: { date: string; time_period: string } + period_end: { date: string; time_period: string } + interval: string + } + onDateRangeChange?: (from: string, to: string) => void +} + +export const WithMonitor = ({ dateRange, onDateRangeChange }: WithMonitorProps) => { + const { ref } = useParams() + const [selectedQuery, setSelectedQuery] = useState(null) + + // [kemal]: Fetch pg_stat_monitor logs. This will need to change when we move to the actual extension. + const effectiveDateRange = useMemo(() => { + if (dateRange) { + return { + iso_timestamp_start: dateRange.period_start.date, + iso_timestamp_end: dateRange.period_end.date, + } + } + + // [kemal]: Fallback to default 24 hours + const end = dayjs.utc() + const start = end.subtract(24, 'hours') + return { + iso_timestamp_start: start.toISOString(), + iso_timestamp_end: end.toISOString(), + } + }, [dateRange]) + + const queryWithTimeRange = useMemo(() => { + return getPgStatMonitorLogsQuery( + effectiveDateRange.iso_timestamp_start, + effectiveDateRange.iso_timestamp_end + ) + }, [effectiveDateRange]) + + const pgStatMonitorLogs = useLogsQuery(ref as string, { + sql: queryWithTimeRange, + iso_timestamp_start: effectiveDateRange.iso_timestamp_start, + iso_timestamp_end: effectiveDateRange.iso_timestamp_end, + }) + + const { logData, isLoading: isLogsLoading, error: logsError } = pgStatMonitorLogs + + const parsedLogs = useMemo(() => { + return parsePgStatMonitorLogs(logData || []) + }, [logData]) + + const chartData = useMemo(() => { + return transformLogsToChartData(parsedLogs) + }, [parsedLogs]) + + const aggregatedGridData = useMemo(() => { + return aggregateLogsByQuery(parsedLogs) + }, [parsedLogs]) + + const handleSelectQuery = (query: string) => { + setSelectedQuery((prev) => (prev === query ? null : query)) + } + + return ( + <> + + + } + /> + + + + ) +} diff --git a/apps/studio/components/interfaces/QueryPerformance/WithMonitor/WithMonitor.utils.ts b/apps/studio/components/interfaces/QueryPerformance/WithMonitor/WithMonitor.utils.ts new file mode 100644 index 0000000000000..f65cfb30930e8 --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformance/WithMonitor/WithMonitor.utils.ts @@ -0,0 +1,301 @@ +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import { transformLogsToJSON } from '../QueryPerformance.utils' +import { QueryPerformanceRow } from '../QueryPerformance.types' + +dayjs.extend(utc) + +export interface ParsedLogEntry { + bucket_start_time?: string + bucket?: string + timestamp?: string + ts?: string + mean_time?: number + mean_exec_time?: number + mean_query_time?: number + min_time?: number + min_exec_time?: number + min_query_time?: number + max_time?: number + max_exec_time?: number + max_query_time?: number + stddev_time?: number + stddev_exec_time?: number + stddev_query_time?: number + rows?: number + calls?: number + shared_blks_hit?: number + shared_blks_read?: number + query?: string + userid?: string + rolname?: string + resp_calls?: number[] + [key: string]: any +} + +export interface ChartDataPoint { + period_start: number + timestamp: string + query_latency: number + mean_time: number + min_time: number + max_time: number + stddev_time: number + p50_time: number + p95_time: number + rows_read: number + calls: number + cache_hits: number + cache_misses: number +} + +export const parsePgStatMonitorLogs = (logData: any[]): ParsedLogEntry[] => { + if (!logData || logData.length === 0) return [] + + const validParsedLogs = logData + .map((log) => ({ + ...log, + parsedEventMessage: transformLogsToJSON(log.event_message), + })) + .filter((log) => log.parsedEventMessage !== null) + .filter((log) => log.parsedEventMessage?.event === 'bucket_query') + + return validParsedLogs.map((log) => log.parsedEventMessage) +} + +export const transformLogsToChartData = (parsedLogs: ParsedLogEntry[]): ChartDataPoint[] => { + if (!parsedLogs || parsedLogs.length === 0) return [] + + // [kemal]: here for debugging + // if (parsedLogs.length > 0) { + // console.log('🟡 Parsed logs:', parsedLogs) + // } + + return parsedLogs + .map((log: ParsedLogEntry) => { + const possibleTimestamps = [log.bucket_start_time, log.bucket, log.timestamp, log.ts] + + let periodStart: number | null = null + + for (const ts of possibleTimestamps) { + if (ts) { + const date = new Date(ts) + const time = date.getTime() + if (!isNaN(time) && time > 0 && time > 946684800000) { + periodStart = time + break + } + } + } + + if (!periodStart) { + return null + } + + const percentiles = + log.resp_calls && Array.isArray(log.resp_calls) + ? calculatePercentilesFromHistogram(log.resp_calls) + : { p50: 0, p95: 0 } + + return { + period_start: periodStart, + timestamp: possibleTimestamps.find((t) => t) || '', + query_latency: parseFloat( + String(log.mean_time ?? log.mean_exec_time ?? log.mean_query_time ?? 0) + ), + mean_time: parseFloat( + String(log.mean_time ?? log.mean_exec_time ?? log.mean_query_time ?? 0) + ), + min_time: parseFloat(String(log.min_time ?? log.min_exec_time ?? log.min_query_time ?? 0)), + max_time: parseFloat(String(log.max_time ?? log.max_exec_time ?? log.max_query_time ?? 0)), + stddev_time: parseFloat( + String(log.stddev_time ?? log.stddev_exec_time ?? log.stddev_query_time ?? 0) + ), + p50_time: percentiles.p50, + p95_time: percentiles.p95, + rows_read: parseInt(String(log.rows ?? 0), 10), + calls: parseInt(String(log.calls ?? 0), 10), + cache_hits: parseFloat(String(log.shared_blks_hit ?? 0)), + cache_misses: parseFloat(String(log.shared_blks_read ?? 0)), + } + }) + .filter((item): item is NonNullable => item !== null) + .sort((a, b) => a.period_start - b.period_start) +} + +const normalizeQuery = (query: string): string => { + return query.replace(/\s+/g, ' ').trim() +} + +export const aggregateLogsByQuery = (parsedLogs: ParsedLogEntry[]): QueryPerformanceRow[] => { + if (!parsedLogs || parsedLogs.length === 0) return [] + + const queryGroups = new Map() + + parsedLogs.forEach((log) => { + const query = normalizeQuery(log.query || '') + if (!query) return + + if (!queryGroups.has(query)) { + queryGroups.set(query, []) + } + queryGroups.get(query)!.push(log) + }) + + const aggregatedData: QueryPerformanceRow[] = [] + let totalExecutionTime = 0 + + const queryStats = Array.from(queryGroups.entries()).map(([query, logs]) => { + const count = logs.length + let totalCalls = 0 + let totalRowsRead = 0 + let totalCacheHits = 0 + let totalCacheMisses = 0 + let rolname = logs[0].username + let minTime = Infinity + let maxTime = -Infinity + let totalExecutionTimeForQuery = 0 + + logs.forEach((log) => { + const logMeanTime = parseFloat( + String(log.mean_time ?? log.mean_exec_time ?? log.mean_query_time ?? 0) + ) + const logMinTime = parseFloat( + String(log.min_time ?? log.min_exec_time ?? log.min_query_time ?? 0) + ) + const logMaxTime = parseFloat( + String(log.max_time ?? log.max_exec_time ?? log.max_query_time ?? 0) + ) + const logCalls = parseInt(String(log.calls ?? 0), 10) + const logRows = parseInt(String(log.rows ?? 0), 10) + const logCacheHits = parseFloat(String(log.shared_blks_hit ?? 0)) + const logCacheMisses = parseFloat(String(log.shared_blks_read ?? 0)) + + minTime = Math.min(minTime, logMinTime) + maxTime = Math.max(maxTime, logMaxTime) + totalCalls += logCalls + totalRowsRead += logRows + totalCacheHits += logCacheHits + totalCacheMisses += logCacheMisses + totalExecutionTimeForQuery += logMeanTime * logCalls + }) + + // Overall mean time is the weighted average + const avgMeanTime = totalCalls > 0 ? totalExecutionTimeForQuery / totalCalls : 0 + const finalMinTime = minTime === Infinity ? 0 : minTime + const finalMaxTime = maxTime === -Infinity ? 0 : maxTime + + totalExecutionTime += totalExecutionTimeForQuery + + return { + query, + rolname, + count, + avgMeanTime, + minTime: finalMinTime, + maxTime: finalMaxTime, + totalCalls, + totalRowsRead, + totalTime: totalExecutionTimeForQuery, + totalCacheHits, + totalCacheMisses, + } + }) + + queryStats.forEach((stats) => { + const totalCacheAccess = stats.totalCacheHits + stats.totalCacheMisses + const cacheHitRate = totalCacheAccess > 0 ? (stats.totalCacheHits / totalCacheAccess) * 100 : 0 + + const propTotalTime = totalExecutionTime > 0 ? (stats.totalTime / totalExecutionTime) * 100 : 0 + + aggregatedData.push({ + query: stats.query, + rolname: stats.rolname, + calls: stats.totalCalls, + mean_time: stats.avgMeanTime, + min_time: stats.minTime, + max_time: stats.maxTime, + total_time: stats.totalTime, + rows_read: stats.totalRowsRead, + cache_hit_rate: cacheHitRate, + prop_total_time: propTotalTime, + index_advisor_result: null, + _total_cache_hits: stats.totalCacheHits, + _total_cache_misses: stats.totalCacheMisses, + _count: stats.count, + }) + }) + + return aggregatedData.sort((a, b) => b.total_time - a.total_time) +} + +export const calculatePercentilesFromHistogram = ( + respCalls: number[] +): { + p50: number + p95: number +} => { + const bucketBoundaries = [ + { min: 0, max: 1 }, + { min: 1, max: 10 }, + { min: 10, max: 100 }, + { min: 100, max: 1000 }, + { min: 1000, max: 10000 }, + { min: 10000, max: 100000 }, + ] + + const totalCalls = respCalls.reduce((sum, count) => sum + count, 0) + + if (totalCalls === 0) { + return { p50: 0, p95: 0 } + } + + const distribution: { + minValue: number + maxValue: number + cumulativeCount: number + count: number + }[] = [] + let cumulativeCount = 0 + + respCalls.forEach((count, index) => { + if (count > 0 && index < bucketBoundaries.length) { + const bucket = bucketBoundaries[index] + cumulativeCount += count + distribution.push({ + minValue: bucket.min, + maxValue: bucket.max, + cumulativeCount, + count, + }) + } + }) + + const getPercentile = (percentile: number): number => { + const targetCount = totalCalls * percentile + + for (let i = 0; i < distribution.length; i++) { + const prevCumulativeCount = i > 0 ? distribution[i - 1].cumulativeCount : 0 + + if (distribution[i].cumulativeCount >= targetCount) { + const positionInBucket = (targetCount - prevCumulativeCount) / distribution[i].count + const bucketMin = distribution[i].minValue + const bucketMax = distribution[i].maxValue + const logMin = Math.log10(Math.max(bucketMin, 0.1)) + const logMax = Math.log10(bucketMax) + const logValue = logMin + positionInBucket * (logMax - logMin) + + return Math.pow(10, logValue) + } + } + + return distribution[distribution.length - 1]?.maxValue || 0 + } + + const result = { + p50: getPercentile(0.5), + p95: getPercentile(0.95), + } + + return result +} diff --git a/apps/studio/components/interfaces/QueryPerformance/WithStatements/WithStatements.tsx b/apps/studio/components/interfaces/QueryPerformance/WithStatements/WithStatements.tsx new file mode 100644 index 0000000000000..b35864d3a4659 --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformance/WithStatements/WithStatements.tsx @@ -0,0 +1,186 @@ +import { LoadingLine, cn } from 'ui' +import { useState, useEffect, useMemo } from 'react' + +import { Button } from 'ui' +import { X, RefreshCw, RotateCcw } from 'lucide-react' +import { Markdown } from '../../Markdown' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' +import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { DOCS_URL, IS_PLATFORM } from 'lib/constants' +import { executeSql } from 'data/sql/execute-sql-query' +import { toast } from 'sonner' +import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' +import { formatDatabaseID } from 'data/read-replicas/replicas.utils' +import { PresetHookResult } from 'components/interfaces/Reports/Reports.utils' +import { DbQueryHook } from 'hooks/analytics/useDbQuery' +import { QueryPerformanceMetrics } from '../QueryPerformanceMetrics' +import { QueryPerformanceFilterBar } from '../QueryPerformanceFilterBar' +import { QueryPerformanceGrid } from '../QueryPerformanceGrid' +import { transformStatementDataToRows } from './WithStatements.utils' +import { DownloadResultsButton } from 'components/ui/DownloadResultsButton' + +interface WithStatementsProps { + queryHitRate: PresetHookResult + queryPerformanceQuery: DbQueryHook + queryMetrics: PresetHookResult +} + +export const WithStatements = ({ + queryHitRate, + queryPerformanceQuery, + queryMetrics, +}: WithStatementsProps) => { + const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() + const state = useDatabaseSelectorStateSnapshot() + const { data, isLoading, isRefetching } = queryPerformanceQuery + const isPrimaryDatabase = state.selectedDatabaseId === ref + const formattedDatabaseId = formatDatabaseID(state.selectedDatabaseId ?? '') + + const [showResetgPgStatStatements, setShowResetgPgStatStatements] = useState(false) + + const [showBottomSection, setShowBottomSection] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.QUERY_PERF_SHOW_BOTTOM_SECTION, + true + ) + + const handleRefresh = () => { + queryPerformanceQuery.runQuery() + queryHitRate.runQuery() + queryMetrics.runQuery() + } + + const processedData = useMemo(() => { + return transformStatementDataToRows(data || []) + }, [data]) + + const { data: databases } = useReadReplicasQuery({ projectRef: ref }) + + useEffect(() => { + state.setSelectedDatabaseId(ref) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref]) + + return ( + <> + + + + + + + } + /> + + +
    + +
    +

    Reset report

    +

    + Consider resetting the analysis after optimizing any queries +

    + +
    + +
    +

    How is this report generated?

    + +
    + +
    +

    Inspect your database for potential issues

    + +
    +
    + + setShowResetgPgStatStatements(false)} + onConfirm={async () => { + const connectionString = databases?.find( + (db) => db.identifier === state.selectedDatabaseId + )?.connectionString + + if (IS_PLATFORM && !connectionString) { + return toast.error('Unable to run query: Connection string is missing') + } + + try { + await executeSql({ + projectRef: project?.ref, + connectionString, + sql: `SELECT pg_stat_statements_reset();`, + }) + handleRefresh() + setShowResetgPgStatStatements(false) + } catch (error: any) { + toast.error(`Failed to reset analysis: ${error.message}`) + } + }} + > +

    + This will reset the pg_stat_statements table in the extensions schema on your{' '} + + {isPrimaryDatabase ? 'primary database' : `read replica (ID: ${formattedDatabaseId})`} + + , which is used to calculate query performance. This data will repopulate immediately + after. +

    +
    + + ) +} diff --git a/apps/studio/components/interfaces/QueryPerformance/WithStatements/WithStatements.utils.ts b/apps/studio/components/interfaces/QueryPerformance/WithStatements/WithStatements.utils.ts new file mode 100644 index 0000000000000..8e5ba0b86f328 --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformance/WithStatements/WithStatements.utils.ts @@ -0,0 +1,22 @@ +import { QueryPerformanceRow } from '../QueryPerformance.types' + +export const transformStatementDataToRows = (data: any[]): QueryPerformanceRow[] => { + if (!data || data.length === 0) return [] + + const totalTimeAcrossAllQueries = data.reduce((sum, row) => sum + (row.total_time || 0), 0) + + return data.map((row) => ({ + query: row.query, + rolname: row.rolname || undefined, + calls: row.calls || 0, + mean_time: row.mean_time || 0, + min_time: row.min_time || 0, + max_time: row.max_time || 0, + total_time: row.total_time || 0, + rows_read: row.rows_read || 0, + cache_hit_rate: row.cache_hit_rate || 0, + prop_total_time: + totalTimeAcrossAllQueries > 0 ? (row.total_time / totalTimeAcrossAllQueries) * 100 : 0, + index_advisor_result: row.index_advisor_result || null, + })) +} diff --git a/apps/studio/components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus.ts b/apps/studio/components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus.ts index 5775541954b01..7001611ce788b 100644 --- a/apps/studio/components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus.ts +++ b/apps/studio/components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus.ts @@ -1,4 +1,4 @@ -import { getIndexAdvisorExtensions } from 'components/interfaces/QueryPerformance/index-advisor.utils' +import { getIndexAdvisorExtensions } from 'components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' diff --git a/apps/studio/components/layouts/AdvisorsLayout/AdvisorsMenu.utils.ts b/apps/studio/components/layouts/AdvisorsLayout/AdvisorsMenu.utils.tsx similarity index 86% rename from apps/studio/components/layouts/AdvisorsLayout/AdvisorsMenu.utils.ts rename to apps/studio/components/layouts/AdvisorsLayout/AdvisorsMenu.utils.tsx index ee22c2d7ba5a2..5566c59aff190 100644 --- a/apps/studio/components/layouts/AdvisorsLayout/AdvisorsMenu.utils.ts +++ b/apps/studio/components/layouts/AdvisorsLayout/AdvisorsMenu.utils.tsx @@ -1,6 +1,7 @@ import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types' import type { Project } from 'data/projects/project-detail-query' import { IS_PLATFORM } from 'lib/constants' +import { ArrowUpRight } from 'lucide-react' export const generateAdvisorsMenu = ( project?: Project, @@ -27,8 +28,9 @@ export const generateAdvisorsMenu = ( { name: 'Query Performance', key: 'query-performance', - url: `/project/${ref}/advisors/query-performance`, + url: `/project/${ref}/reports/query-performance`, items: [], + rightIcon: , }, ], }, diff --git a/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx b/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx index 1895819ffac5f..20d2216ec6e5e 100644 --- a/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx +++ b/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx @@ -77,7 +77,7 @@ export function useReportsGotoCommands(options?: CommandOptions) { { id: QUERY_PERFORMANCE_COMMAND_ID, name: 'Query Performance Reports', - route: `/project/${ref}/advisors/query-performance`, + route: `/project/${ref}/reports/query-performance`, defaultHidden: true, }, ] diff --git a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx index a770362494a59..f0644ef774088 100644 --- a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx +++ b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx @@ -1,5 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Plus, ArrowUpRight } from 'lucide-react' +import { Plus } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' @@ -149,8 +149,7 @@ const ReportsMenu = () => { { name: 'Query Performance', key: 'query-performance', - url: `/project/${ref}/advisors/query-performance`, - rightIcon: , + url: `/project/${ref}/reports/query-performance${preservedQueryParams}`, }, ...(postgrestReportEnabled ? [ @@ -261,9 +260,6 @@ const ReportsMenu = () => { className="flex-grow h-7 flex justify-between items-center pl-3" > {subItem.name} - {subItem.rightIcon && ( - {subItem.rightIcon} - )}
  • ))} diff --git a/apps/studio/components/ui/Charts/ChartHeader.tsx b/apps/studio/components/ui/Charts/ChartHeader.tsx index 86a8383414632..94df3ecdab928 100644 --- a/apps/studio/components/ui/Charts/ChartHeader.tsx +++ b/apps/studio/components/ui/Charts/ChartHeader.tsx @@ -9,6 +9,7 @@ import { import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' +import { cn } from 'ui' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' @@ -26,6 +27,8 @@ export interface ChartHeaderProps { highlightedLabel?: number | string | any | null highlightedValue?: number | string | any | null hideHighlightedValue?: boolean + hideHighlightedLabel?: boolean + hideHighlightArea?: boolean hideChartType?: boolean chartStyle?: string onChartStyleChange?: (style: string) => void @@ -50,6 +53,8 @@ export const ChartHeader = ({ highlightedValue, highlightedLabel, hideHighlightedValue = false, + hideHighlightedLabel = false, + hideHighlightArea = false, title, minimalHeader = false, hideChartType = false, @@ -202,11 +207,14 @@ export const ChartHeader = ({ if (minimalHeader) { return ( -
    +
    {title && chartTitle}
    {highlightedValue !== undefined && !hideHighlightedValue && highlighted} - {label} + {!hideHighlightedLabel && label}
    ) @@ -215,12 +223,17 @@ export const ChartHeader = ({ const hasHighlightedValue = highlightedValue !== undefined && !hideHighlightedValue return ( -
    +
    {title && chartTitle}
    {hasHighlightedValue && highlighted} - {label} + {!hideHighlightedLabel && label}
    diff --git a/apps/studio/components/ui/Charts/ComposedChart.tsx b/apps/studio/components/ui/Charts/ComposedChart.tsx index 94ef08ac6f980..acbcfc62db54c 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.tsx @@ -62,6 +62,8 @@ export interface ComposedChartProps extends CommonChartProps { titleTooltip?: string hideYAxis?: boolean hideHighlightedValue?: boolean + hideHighlightedLabel?: boolean + hideHighlightArea?: boolean syncId?: string docsUrl?: string sql?: string @@ -101,6 +103,8 @@ export function ComposedChart({ updateDateRange, hideYAxis, hideHighlightedValue, + hideHighlightedLabel = false, + hideHighlightArea = false, syncId, docsUrl, sql, @@ -316,6 +320,8 @@ export function ComposedChart({ hideHighlightedValue={hideHighlightedValue} title={title} format={format} + hideHighlightedLabel={hideHighlightedLabel} + hideHighlightArea={hideHighlightArea} titleTooltip={titleTooltip} customDateFormat={customDateFormat} highlightedValue={formatHighlightedValue(resolvedHighlightedValue)} diff --git a/apps/studio/pages/project/[ref]/advisors/query-performance.tsx b/apps/studio/pages/project/[ref]/reports/query-performance.tsx similarity index 61% rename from apps/studio/pages/project/[ref]/advisors/query-performance.tsx rename to apps/studio/pages/project/[ref]/reports/query-performance.tsx index 4db0331e91116..f0a78fb3f4d76 100644 --- a/apps/studio/pages/project/[ref]/advisors/query-performance.tsx +++ b/apps/studio/pages/project/[ref]/reports/query-performance.tsx @@ -1,7 +1,7 @@ import { parseAsArrayOf, parseAsString, useQueryStates } from 'nuqs' import { useParams } from 'common' -import { EnableIndexAdvisorButton } from 'components/interfaces/QueryPerformance/EnableIndexAdvisorButton' +import { EnableIndexAdvisorButton } from 'components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton' import { useIndexAdvisorStatus } from 'components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus' import { useQueryPerformanceSort } from 'components/interfaces/QueryPerformance/hooks/useQueryPerformanceSort' import { QueryPerformance } from 'components/interfaces/QueryPerformance/QueryPerformance' @@ -9,19 +9,32 @@ import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants' import { useQueryPerformanceQuery } from 'components/interfaces/Reports/Reports.queries' import { Presets } from 'components/interfaces/Reports/Reports.types' import { queriesFactory } from 'components/interfaces/Reports/Reports.utils' -import AdvisorsLayout from 'components/layouts/AdvisorsLayout/AdvisorsLayout' +import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import DatabaseSelector from 'components/ui/DatabaseSelector' import { DocsButton } from 'components/ui/DocsButton' import { FormHeader } from 'components/ui/Forms/FormHeader' import { DOCS_URL } from 'lib/constants' import type { NextPageWithLayout } from 'types' +import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers' +import { useReportDateRange } from 'hooks/misc/useReportDateRange' +import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' const QueryPerformanceReport: NextPageWithLayout = () => { const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() const { isIndexAdvisorEnabled } = useIndexAdvisorStatus() const { sort: sortConfig } = useQueryPerformanceSort() + const { + selectedDateRange, + datePickerValue, + datePickerHelpers, + updateDateRange, + handleDatePickerChange, + } = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES) + const [{ search: searchQuery, roles }] = useQueryStates({ sort: parseAsString, order: parseAsString, @@ -42,18 +55,32 @@ const QueryPerformanceReport: NextPageWithLayout = () => { runIndexAdvisor: isIndexAdvisorEnabled, }) + const isPgStatMonitorEnabled = project?.dbVersion === '17.4.1.076-psml-1' + return (
    +
    + {isPgStatMonitorEnabled && ( + + h.text === REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES || + h.text === REPORT_DATERANGE_HELPER_LABELS.LAST_3_HOURS || + h.text === REPORT_DATERANGE_HELPER_LABELS.LAST_24_HOURS + )} + onSubmit={handleDatePickerChange} + /> + )}
    } /> @@ -61,6 +88,9 @@ const QueryPerformanceReport: NextPageWithLayout = () => { queryHitRate={queryHitRate} queryPerformanceQuery={queryPerformanceQuery} queryMetrics={queryMetrics} + isPgStatMonitorEnabled={isPgStatMonitorEnabled} + dateRange={selectedDateRange} + onDateRangeChange={updateDateRange} />
    ) @@ -68,7 +98,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => { QueryPerformanceReport.getLayout = (page) => ( - {page} + {page} ) From 56d40fe0b2f162714f68d59c0d98a18d127d282f Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Wed, 15 Oct 2025 17:35:24 +0300 Subject: [PATCH 3/4] chore: Migrate eslint for all apps to use flat config (#39486) * Use the "eslint" command instead of built-in next lint since it's getting obsolete. * Bump all deps to support eslint 9+. * Convert the rules in eslint-config-supabase to be flat-config compatible. * Migrate all apps to use the new eslint config rules. * Fix all errors found in the new setup. * Fix the no default exports ignores. * Scan all files for linting in studio. * Fix all lint errors. * Make the reportUnusedDisableDirectives a warning. --- apps/cms/.eslintrc.cjs | 3 - apps/cms/eslint.config.cjs | 4 + apps/cms/package.json | 2 +- apps/design-system/.eslintrc.cjs | 3 - apps/design-system/eslint.config.cjs | 4 + apps/design-system/package.json | 2 +- apps/docs/.eslintrc.cjs | 3 - apps/docs/eslint.config.cjs | 4 + apps/docs/package.json | 2 +- apps/docs/turbo.json | 3 + apps/studio/.eslintrc.js | 10 - .../Auth/EmailTemplates/TemplateEditor.tsx | 1 - .../CostControl/SpendCapSidePanel.tsx | 1 - .../interfaces/Organization/Usage/Usage.tsx | 1 - .../QueryPerformance/QueryPerformanceGrid.tsx | 13 +- .../StorageExplorer/StorageExplorer.tsx | 1 - .../interfaces/UnifiedLogs/UnifiedLogs.tsx | 1 - .../analytics/infra-monitoring-queries.ts | 5 +- .../analytics/project-daily-stats-queries.ts | 3 +- apps/studio/data/graphql/fragment-masking.ts | 3 +- apps/studio/data/graphql/gql.ts | 1 - apps/studio/data/graphql/graphql.ts | 1 - apps/studio/data/graphql/index.ts | 4 +- apps/studio/eslint.config.cjs | 19 + apps/studio/package.json | 2 +- apps/studio/pages/_document.tsx | 1 - .../project/[ref]/advisors/performance.tsx | 1 - apps/studio/types/index.ts | 16 +- apps/ui-library/.eslintrc.cjs | 3 - apps/ui-library/eslint.config.cjs | 4 + apps/ui-library/package.json | 2 +- apps/www/.eslintrc.js | 9 - apps/www/eslint.config.cjs | 14 + apps/www/internals/generate-sitemap.mjs | 2 - apps/www/package.json | 2 +- package.json | 2 +- packages/eslint-config-supabase/next.js | 55 +- packages/eslint-config-supabase/package.json | 8 +- pnpm-lock.yaml | 489 ++++++++---------- turbo.json | 8 + 40 files changed, 364 insertions(+), 348 deletions(-) delete mode 100644 apps/cms/.eslintrc.cjs create mode 100644 apps/cms/eslint.config.cjs delete mode 100644 apps/design-system/.eslintrc.cjs create mode 100644 apps/design-system/eslint.config.cjs delete mode 100644 apps/docs/.eslintrc.cjs create mode 100644 apps/docs/eslint.config.cjs delete mode 100644 apps/studio/.eslintrc.js create mode 100644 apps/studio/eslint.config.cjs delete mode 100644 apps/ui-library/.eslintrc.cjs create mode 100644 apps/ui-library/eslint.config.cjs delete mode 100644 apps/www/.eslintrc.js create mode 100644 apps/www/eslint.config.cjs diff --git a/apps/cms/.eslintrc.cjs b/apps/cms/.eslintrc.cjs deleted file mode 100644 index 4f1d845f8cfdc..0000000000000 --- a/apps/cms/.eslintrc.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['eslint-config-supabase/next'], -} diff --git a/apps/cms/eslint.config.cjs b/apps/cms/eslint.config.cjs new file mode 100644 index 0000000000000..fec3ac9b3ab84 --- /dev/null +++ b/apps/cms/eslint.config.cjs @@ -0,0 +1,4 @@ +const { defineConfig } = require('eslint/config') +const supabaseConfig = require('eslint-config-supabase/next') + +module.exports = defineConfig([supabaseConfig]) diff --git a/apps/cms/package.json b/apps/cms/package.json index 25169cc2c0b1c..cc7a53cf52a81 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -12,7 +12,7 @@ "devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev", "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", - "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", + "lint": "eslint .", "migrate": "cross-env NODE_OPTIONS=--no-deprecation tsx scripts/migrate.ts", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", "start": "cross-env NODE_OPTIONS=--no-deprecation next start", diff --git a/apps/design-system/.eslintrc.cjs b/apps/design-system/.eslintrc.cjs deleted file mode 100644 index 4f1d845f8cfdc..0000000000000 --- a/apps/design-system/.eslintrc.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['eslint-config-supabase/next'], -} diff --git a/apps/design-system/eslint.config.cjs b/apps/design-system/eslint.config.cjs new file mode 100644 index 0000000000000..fec3ac9b3ab84 --- /dev/null +++ b/apps/design-system/eslint.config.cjs @@ -0,0 +1,4 @@ +const { defineConfig } = require('eslint/config') +const supabaseConfig = require('eslint-config-supabase/next') + +module.exports = defineConfig([supabaseConfig]) diff --git a/apps/design-system/package.json b/apps/design-system/package.json index daeefc6cec0d0..cc8cd3e77cd06 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -10,7 +10,7 @@ "build": "pnpm run content:build && pnpm run build:registry && next build --turbopack", "build:registry": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache", "start": "next start", - "lint": "next lint", + "lint": "eslint .", "content:dev": "contentlayer2 dev", "content:build": "contentlayer2 build", "clean": "rimraf node_modules .next .turbo", diff --git a/apps/docs/.eslintrc.cjs b/apps/docs/.eslintrc.cjs deleted file mode 100644 index 4f1d845f8cfdc..0000000000000 --- a/apps/docs/.eslintrc.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['eslint-config-supabase/next'], -} diff --git a/apps/docs/eslint.config.cjs b/apps/docs/eslint.config.cjs new file mode 100644 index 0000000000000..fec3ac9b3ab84 --- /dev/null +++ b/apps/docs/eslint.config.cjs @@ -0,0 +1,4 @@ +const { defineConfig } = require('eslint/config') +const supabaseConfig = require('eslint-config-supabase/next') + +module.exports = defineConfig([supabaseConfig]) diff --git a/apps/docs/package.json b/apps/docs/package.json index 17a773cb51dc5..e085fcb357aa4 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -22,7 +22,7 @@ "embeddings:nimbus:refresh": "ENABLED_FEATURES_OVERRIDE_DISABLE_ALL=true pnpm run embeddings:refresh", "last-changed": "tsx scripts/last-changed.ts", "last-changed:reset": "pnpm run last-changed -- --reset", - "lint": "next lint", + "lint": "eslint .", "lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml", "postbuild": "pnpm run build:sitemap && pnpm run build:llms && ./../../scripts/upload-static-assets.sh", "prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples", diff --git a/apps/docs/turbo.json b/apps/docs/turbo.json index 5e22a06e3d6bc..817dd4625c712 100644 --- a/apps/docs/turbo.json +++ b/apps/docs/turbo.json @@ -27,6 +27,9 @@ "NEXT_PUBLIC_SENTRY_DSN", "NEXT_PUBLIC_VERCEL_ENV", "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA", + "VERCEL", + "VERCEL_ENV", + "VERCEL_GIT_COMMIT_SHA", // These envs are used in the packages "NEXT_PUBLIC_STORAGE_KEY", "NEXT_PUBLIC_AUTH_DEBUG_KEY", diff --git a/apps/studio/.eslintrc.js b/apps/studio/.eslintrc.js deleted file mode 100644 index 45017c942f867..0000000000000 --- a/apps/studio/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - extends: ['eslint-config-supabase/next'], - plugins: ['eslint-plugin-barrel-files'], - rules: { - '@next/next/no-img-element': 'off', - 'react/no-unescaped-entities': 'off', - 'react/display-name': 'warn', - 'barrel-files/avoid-re-export-all': 'error', - }, -} diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx index 0ef99c3b567ed..e70e547934be4 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx @@ -76,7 +76,6 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) const [isSavingTemplate, setIsSavingTemplate] = useState(false) - // eslint-disable-next-line react-hooks/exhaustive-deps const spamRules = (validationResult?.rules ?? []).filter((rule) => rule.score > 0) const preventSaveFromSpamCheck = builtInSMTP && spamRules.length > 0 diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx index 6051d14608b12..160d294d00ee5 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx @@ -74,7 +74,6 @@ const SpendCapSidePanel = () => { if (visible && subscription !== undefined) { setSelectedOption(isSpendCapOn ? 'on' : 'off') } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, isLoading, subscription, isSpendCapOn]) const onConfirm = async () => { diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index 7d9aee83ae9f9..17644f6ff2f78 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -127,7 +127,6 @@ export const Usage = () => { setSelectedProjectRefInputValue(projectRef) } // [Joshen] Since we're already looking at isSuccess - // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectRef, isSuccessProjectDetail]) return ( diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx index e680c93d09dba..a35322a52def4 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx @@ -1,26 +1,28 @@ -import { ArrowDown, ArrowUp, ChevronDown, ArrowRight, TextSearch } from 'lucide-react' +import { ArrowDown, ArrowRight, ArrowUp, ChevronDown, TextSearch } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid' import { useParams } from 'common' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { Button, + CodeBlock, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Sheet, SheetContent, + SheetTitle, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_, Tabs_Shadcn_, cn, - CodeBlock, - SheetTitle, } from 'ui' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort' import { hasIndexRecommendations } from './IndexAdvisor/index-advisor.utils' import { IndexSuggestionIcon } from './IndexAdvisor/IndexSuggestionIcon' import { QueryDetail } from './QueryDetail' @@ -30,10 +32,8 @@ import { QUERY_PERFORMANCE_REPORT_TYPES, QUERY_PERFORMANCE_ROLE_DESCRIPTION, } from './QueryPerformance.constants' -import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort' -import { formatDuration } from './QueryPerformance.utils' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { QueryPerformanceRow } from './QueryPerformance.types' +import { formatDuration } from './QueryPerformance.utils' interface QueryPerformanceGridProps { aggregatedData: QueryPerformanceRow[] @@ -369,7 +369,6 @@ export const QueryPerformanceGrid = ({ useEffect(() => { setSelectedRow(undefined) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [search, roles, urlSort, order]) const handleKeyDown = useCallback( diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx index e827c71f24d10..c6471fd917445 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx @@ -53,7 +53,6 @@ export const StorageExplorer = ({ bucket }: StorageExplorerProps) => { // Things like showing results from a search filter is "temporary", hence we use react state to manage const [itemSearchString, setItemSearchString] = useState('') - // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { const fetchContents = async () => { if (view === STORAGE_VIEWS.LIST) { diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx index d2250d0576ab9..328489afa925a 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx @@ -285,7 +285,6 @@ export const UnifiedLogs = () => { useEffect(() => { debouncedApplyFilterSearch() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [columnFilters, debouncedApplyFilterSearch]) useEffect(() => { diff --git a/apps/studio/data/analytics/infra-monitoring-queries.ts b/apps/studio/data/analytics/infra-monitoring-queries.ts index dad4edd42583d..7cc001a93900d 100644 --- a/apps/studio/data/analytics/infra-monitoring-queries.ts +++ b/apps/studio/data/analytics/infra-monitoring-queries.ts @@ -1,6 +1,6 @@ -import { useInfraMonitoringQuery } from './infra-monitoring-query' -import type { InfraMonitoringAttribute } from './infra-monitoring-query' import { AnalyticsInterval } from './constants' +import type { InfraMonitoringAttribute } from './infra-monitoring-query' +import { useInfraMonitoringQuery } from './infra-monitoring-query' export function useInfraMonitoringQueries( attributes: InfraMonitoringAttribute[], @@ -13,6 +13,7 @@ export function useInfraMonitoringQueries( isVisible: boolean ) { return attributes.map((attribute) => + // eslint-disable-next-line react-hooks/rules-of-hooks useInfraMonitoringQuery( { projectRef: ref as string, diff --git a/apps/studio/data/analytics/project-daily-stats-queries.ts b/apps/studio/data/analytics/project-daily-stats-queries.ts index 92e29be93f81c..d039864975993 100644 --- a/apps/studio/data/analytics/project-daily-stats-queries.ts +++ b/apps/studio/data/analytics/project-daily-stats-queries.ts @@ -1,5 +1,5 @@ -import { useProjectDailyStatsQuery } from './project-daily-stats-query' import type { ProjectDailyStatsAttribute } from './project-daily-stats-query' +import { useProjectDailyStatsQuery } from './project-daily-stats-query' export function useProjectDailyStatsQueries( attributes: ProjectDailyStatsAttribute[], @@ -10,6 +10,7 @@ export function useProjectDailyStatsQueries( isVisible: boolean ) { return attributes.map((attribute) => + // eslint-disable-next-line react-hooks/rules-of-hooks useProjectDailyStatsQuery( { projectRef: ref as string, diff --git a/apps/studio/data/graphql/fragment-masking.ts b/apps/studio/data/graphql/fragment-masking.ts index 494960811faac..20d21c67a3a39 100644 --- a/apps/studio/data/graphql/fragment-masking.ts +++ b/apps/studio/data/graphql/fragment-masking.ts @@ -1,5 +1,4 @@ -/* eslint-disable */ -import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core' +import { DocumentTypeDecoration, ResultOf } from '@graphql-typed-document-node/core' import { Incremental, TypedDocumentString } from './graphql' export type FragmentType> = diff --git a/apps/studio/data/graphql/gql.ts b/apps/studio/data/graphql/gql.ts index 1f4ff76f6e6ff..9164d77dbcdcf 100644 --- a/apps/studio/data/graphql/gql.ts +++ b/apps/studio/data/graphql/gql.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ import * as types from './graphql' /** diff --git a/apps/studio/data/graphql/graphql.ts b/apps/studio/data/graphql/graphql.ts index 99d32bef6b994..aac84b54d9967 100644 --- a/apps/studio/data/graphql/graphql.ts +++ b/apps/studio/data/graphql/graphql.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ import { DocumentTypeDecoration } from '@graphql-typed-document-node/core' export type Maybe = T | null export type InputMaybe = Maybe diff --git a/apps/studio/data/graphql/index.ts b/apps/studio/data/graphql/index.ts index f9bc8e591d00f..83851531f643a 100644 --- a/apps/studio/data/graphql/index.ts +++ b/apps/studio/data/graphql/index.ts @@ -1,2 +1,2 @@ -export * from './fragment-masking' -export * from './gql' +export {} from './fragment-masking' +export { graphql } from './gql' diff --git a/apps/studio/eslint.config.cjs b/apps/studio/eslint.config.cjs new file mode 100644 index 0000000000000..5b3f0df7fd398 --- /dev/null +++ b/apps/studio/eslint.config.cjs @@ -0,0 +1,19 @@ +const { defineConfig } = require('eslint/config') +const barrelFiles = require('eslint-plugin-barrel-files') +const supabaseConfig = require('eslint-config-supabase/next') + +module.exports = defineConfig([ + { files: ['**/*.ts', '**/*.tsx'] }, + supabaseConfig, + { + plugins: { + 'barrel-files': barrelFiles, + }, + rules: { + '@next/next/no-img-element': 'off', + 'react/no-unescaped-entities': 'off', + 'react/display-name': 'warn', + 'barrel-files/avoid-re-export-all': 'error', + }, + }, +]) diff --git a/apps/studio/package.json b/apps/studio/package.json index 5a0b527c809ee..842f137b5faf8 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -7,7 +7,7 @@ "dev": "next dev --turbopack -p 8082", "build": "next build && ./../../scripts/upload-static-assets.sh", "start": "next start", - "lint": "next lint", + "lint": "eslint .", "clean": "rimraf node_modules tsconfig.tsbuildinfo .next .turbo", "test": "vitest --run --coverage", "test:watch": "vitest watch", diff --git a/apps/studio/pages/_document.tsx b/apps/studio/pages/_document.tsx index c8c36e2c3486f..e91481eb0f32f 100644 --- a/apps/studio/pages/_document.tsx +++ b/apps/studio/pages/_document.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @next/next/no-css-tags */ import { BASE_PATH, IS_PLATFORM } from 'lib/constants' import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document' diff --git a/apps/studio/pages/project/[ref]/advisors/performance.tsx b/apps/studio/pages/project/[ref]/advisors/performance.tsx index 2e830a927cf5a..9d87e820be386 100644 --- a/apps/studio/pages/project/[ref]/advisors/performance.tsx +++ b/apps/studio/pages/project/[ref]/advisors/performance.tsx @@ -35,7 +35,6 @@ const ProjectLints: NextPageWithLayout = () => { const activeLints = useMemo(() => { return [...(data ?? [])]?.filter((x) => x.categories.includes('PERFORMANCE')) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]) const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters || []) as string[] diff --git a/apps/studio/types/index.ts b/apps/studio/types/index.ts index cacc37f912d10..e53ca46b6e038 100644 --- a/apps/studio/types/index.ts +++ b/apps/studio/types/index.ts @@ -1,6 +1,14 @@ -export * from './base' -export type * from './ui' -export type * from './userContent' +export { + ResponseError, + type Dictionary, + type Organization, + type Permission, + type ResponseFailure, + type Role, + type SupaResponse, +} from './base' +export type * from './form' export type * from './next' export { isNextPageWithLayout } from './next' -export type * from './form' +export type * from './ui' +export type * from './userContent' diff --git a/apps/ui-library/.eslintrc.cjs b/apps/ui-library/.eslintrc.cjs deleted file mode 100644 index 4f1d845f8cfdc..0000000000000 --- a/apps/ui-library/.eslintrc.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['eslint-config-supabase/next'], -} diff --git a/apps/ui-library/eslint.config.cjs b/apps/ui-library/eslint.config.cjs new file mode 100644 index 0000000000000..fec3ac9b3ab84 --- /dev/null +++ b/apps/ui-library/eslint.config.cjs @@ -0,0 +1,4 @@ +const { defineConfig } = require('eslint/config') +const supabaseConfig = require('eslint-config-supabase/next') + +module.exports = defineConfig([supabaseConfig]) diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json index c398bbeeb9f8f..05c09a053f4b6 100644 --- a/apps/ui-library/package.json +++ b/apps/ui-library/package.json @@ -10,7 +10,7 @@ "build:registry": "rimraf -G public/r/* && tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts && shadcn build public/r/registry.json && tsx scripts/clean-registry.ts", "build:llms": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-llms-txt.ts", "start": "next start", - "lint": "next lint", + "lint": "eslint .", "lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml", "content:build": "contentlayer2 build", "clean": "rimraf node_modules .next .turbo", diff --git a/apps/www/.eslintrc.js b/apps/www/.eslintrc.js deleted file mode 100644 index d6f7ef0c0539d..0000000000000 --- a/apps/www/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - extends: ['eslint-config-supabase/next'], - rules: { - 'react-hooks/rules-of-hooks': 'warn', - 'react/no-unescaped-entities': 'warn', - 'react/display-name': 'warn', - 'react/no-children-prop': 'warn', - }, -} diff --git a/apps/www/eslint.config.cjs b/apps/www/eslint.config.cjs new file mode 100644 index 0000000000000..8ebf71dad2af9 --- /dev/null +++ b/apps/www/eslint.config.cjs @@ -0,0 +1,14 @@ +const { defineConfig } = require('eslint/config') +const supabaseConfig = require('eslint-config-supabase/next') + +module.exports = defineConfig([ + supabaseConfig, + { + rules: { + 'react-hooks/rules-of-hooks': 'warn', + 'react/no-unescaped-entities': 'warn', + 'react/display-name': 'warn', + 'react/no-children-prop': 'warn', + }, + }, +]) diff --git a/apps/www/internals/generate-sitemap.mjs b/apps/www/internals/generate-sitemap.mjs index d884aa295fc39..f8231787875ac 100644 --- a/apps/www/internals/generate-sitemap.mjs +++ b/apps/www/internals/generate-sitemap.mjs @@ -220,9 +220,7 @@ async function generate() { /** * write sitemaps */ - // eslint-disable-next-line no-sync writeFileSync('public/sitemap.xml', sitemapRouter) - // eslint-disable-next-line no-sync writeFileSync('public/sitemap_www.xml', formatted) } diff --git a/apps/www/package.json b/apps/www/package.json index ad034c28b758e..dc40a6c0c6bca 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -9,7 +9,7 @@ "build": "pnpm run content:build && next build", "export": "next export", "start": "next start", - "lint": "next lint", + "lint": "eslint .", "clean": "rimraf node_modules", "pretypecheck": "next typegen", "typecheck": "pnpm run content:build && tsc --noEmit", diff --git a/package.json b/package.json index d8b192fe306d6..9e900a392dafd 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "devDependencies": { "@aws-sdk/client-secrets-manager": "^3.410.0", "@types/node": "catalog:", - "eslint": "^8.57.0", + "eslint": "^9.0.0", "prettier": "3.2.4", "prettier-plugin-sql-cst": "^0.11.0", "rimraf": "^6.0.0", diff --git a/packages/eslint-config-supabase/next.js b/packages/eslint-config-supabase/next.js index f3f39c45b6639..b19ed52548189 100644 --- a/packages/eslint-config-supabase/next.js +++ b/packages/eslint-config-supabase/next.js @@ -1,16 +1,43 @@ -module.exports = { - extends: ['prettier', 'next/core-web-vitals', 'eslint-config-turbo'], - rules: { - '@next/next/no-html-link-for-pages': 'off', - 'react/jsx-key': 'off', - 'no-restricted-exports': ['warn', { restrictDefaultExports: { direct: true } }], +const { defineConfig } = require('eslint/config') +const js = require('@eslint/js') +const { FlatCompat } = require('@eslint/eslintrc') +const prettierConfig = require('eslint-config-prettier/flat') +const { default: turboConfig } = require('eslint-config-turbo/flat') +const { off } = require('process') + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +module.exports = defineConfig([ + // Global ignore for the .next folder + { ignores: ['.next', 'public'] }, + turboConfig, + prettierConfig, + { + extends: compat.extends('next/core-web-vitals'), + linterOptions: { + reportUnusedDisableDirectives: 'warn', + }, + rules: { + '@next/next/no-html-link-for-pages': 'off', + 'react/jsx-key': 'off', + }, }, - overrides: [ - { - files: ['pages/**', 'app/**'], - rules: { - 'no-restricted-exports': 'off', - }, + { + // check for default exports in all files except app and pages folders. + ignores: ['pages/**.tsx', 'app/**.tsx'], + rules: { + 'no-restricted-exports': [ + 'warn', + { + restrictDefaultExports: { + direct: true, + }, + }, + ], }, - ], -} + }, +]) diff --git a/packages/eslint-config-supabase/package.json b/packages/eslint-config-supabase/package.json index e278f95c21c5d..5610d5edeef11 100644 --- a/packages/eslint-config-supabase/package.json +++ b/packages/eslint-config-supabase/package.json @@ -8,11 +8,13 @@ "clean": "rimraf node_modules" }, "dependencies": { - "eslint-config-next": "15.3.1", - "eslint-config-prettier": "^9.1.0", - "eslint-config-turbo": "^2.0.4" + "eslint-config-next": "^15.5.0", + "eslint-config-prettier": "^10.0.0", + "eslint-config-turbo": "^2.5.0" }, "devDependencies": { + "@eslint/eslintrc": "^3.0.0", + "@eslint/js": "^9.0.0", "typescript": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f8426a1e62b9..655a22e78d140 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,8 +68,8 @@ importers: specifier: 'catalog:' version: 22.13.14 eslint: - specifier: ^8.57.0 - version: 8.57.0(supports-color@8.1.1) + specifier: ^9.0.0 + version: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) prettier: specifier: 3.2.4 version: 3.2.4 @@ -1200,7 +1200,7 @@ importers: version: link:../../packages/eslint-config-supabase eslint-plugin-barrel-files: specifier: ^2.0.7 - version: 2.0.7(eslint@8.57.0(supports-color@8.1.1)) + version: 2.0.7(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) graphql-ws: specifier: 5.14.1 version: 5.14.1(graphql@16.11.0) @@ -1820,7 +1820,7 @@ importers: version: 1.15.4 nuxt: specifier: ^4.0.3 - version: 4.1.2(@electric-sql/pglite@0.2.15)(@parcel/watcher@2.5.1)(@types/node@22.13.14)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(pg@8.16.3))(encoding@0.1.13)(eslint@8.57.0(supports-color@8.1.1))(ioredis@5.7.0(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@6.3.6(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) + version: 4.1.2(@electric-sql/pglite@0.2.15)(@parcel/watcher@2.5.1)(@types/node@22.13.14)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(pg@8.16.3))(encoding@0.1.13)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(ioredis@5.7.0(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@6.3.6(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) devDependencies: shadcn: specifier: ^3.0.0 @@ -2057,15 +2057,21 @@ importers: packages/eslint-config-supabase: dependencies: eslint-config-next: - specifier: 15.3.1 - version: 15.3.1(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + specifier: ^15.5.0 + version: 15.5.4(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) eslint-config-prettier: - specifier: ^9.1.0 - version: 9.1.0(eslint@8.57.0(supports-color@8.1.1)) + specifier: ^10.0.0 + version: 10.1.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) eslint-config-turbo: - specifier: ^2.0.4 - version: 2.0.4(eslint@8.57.0(supports-color@8.1.1)) + specifier: ^2.5.0 + version: 2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3) devDependencies: + '@eslint/eslintrc': + specifier: ^3.0.0 + version: 3.3.1(supports-color@8.1.1) + '@eslint/js': + specifier: ^9.0.0 + version: 9.37.0 typescript: specifier: 'catalog:' version: 5.9.2 @@ -2121,7 +2127,7 @@ importers: version: 8.11.11 '@vitest/coverage-v8': specifier: ^3.0.9 - version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9) npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -2357,7 +2363,7 @@ importers: version: 15.5.7 '@vitest/coverage-v8': specifier: ^3.0.9 - version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9) common: specifier: workspace:* version: link:../common @@ -3748,14 +3754,14 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -3764,17 +3770,33 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-community/regexpp@4.9.1': - resolution: {integrity: sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-helpers@0.4.0': + resolution: {integrity: sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@8.57.0': - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/core@0.16.0': + resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.37.0': + resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.0': + resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} @@ -4212,10 +4234,13 @@ packages: peerDependencies: react-hook-form: ^7.0.0 - '@humanwhocodes/config-array@0.11.14': - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -4225,9 +4250,9 @@ packages: resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} engines: {node: '>=10.10.0'} - '@humanwhocodes/object-schema@2.0.2': - resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} - deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -4738,8 +4763,8 @@ packages: '@next/env@15.5.2': resolution: {integrity: sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==} - '@next/eslint-plugin-next@15.3.1': - resolution: {integrity: sha512-oEs4dsfM6iyER3jTzMm4kDSbrQJq8wZw5fmT6fg2V3SMo+kgG+cShzLfEV20senZzv8VF+puNLheiGPlBGsv2A==} + '@next/eslint-plugin-next@15.5.4': + resolution: {integrity: sha512-SR1vhXNNg16T4zffhJ4TS7Xn7eq4NfKfcOsRwea7RIAHrjRpI9ALYbamqIJqkAhowLlERffiwk0FMvTLNdnVtw==} '@next/mdx@15.3.1': resolution: {integrity: sha512-dnpuJRfqqCPFfLDy2hIej41JAl424zk1JOgRd7jjWu2aTeX6oi0gXdcnMAK4lhf7Xl9zSkL2stzDc1YtlB1xyg==} @@ -10303,10 +10328,6 @@ packages: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} - array.prototype.flatmap@1.3.2: - resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} - engines: {node: '>= 0.4'} - array.prototype.flatmap@1.3.3: resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} engines: {node: '>= 0.4'} @@ -12212,8 +12233,8 @@ packages: resolution: {integrity: sha512-FVVO4DNEPub7/VWGERJytT+tllOP2SRt8rdlcneHNWXSECMU7Aj1nXwGfhhsL3h1aZGJbYokIe27wyN7CJpAfA==} engines: {node: '>= 10'} - eslint-config-next@15.3.1: - resolution: {integrity: sha512-GnmyVd9TE/Ihe3RrvcafFhXErErtr2jS0JDeCSp3vWvy86AXwHsRBt0E3MqP/m8ACS1ivcsi5uaqjbhsG18qKw==} + eslint-config-next@15.5.4: + resolution: {integrity: sha512-BzgVVuT3kfJes8i2GHenC1SRJ+W3BTML11lAOYFOOPzrk2xp66jBOAGEFRw+3LkYCln5UzvFsLhojrshb5Zfaw==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' @@ -12221,16 +12242,17 @@ packages: typescript: optional: true - eslint-config-prettier@9.1.0: - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' - eslint-config-turbo@2.0.4: - resolution: {integrity: sha512-zGvU+bxoNWVvSl0prGItrnH9FgeNzKEAjRmv8ruqql1psI37T8IoLF/XeOzT3CzzYzJxuI3wW1yb2agDFYQdHQ==} + eslint-config-turbo@2.5.8: + resolution: {integrity: sha512-wzxmN7dJNFGDwOvR/4j8U2iaIH/ruYez8qg/sCKrezJ3+ljbFMvJLmgKKt/1mDuyU9wj5aZqO6VijP3QH169FA==} peerDependencies: eslint: '>6.6.0' + turbo: '>2.0.0' eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} @@ -12263,27 +12285,6 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-module-utils@2.8.1: - resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - eslint-plugin-barrel-files@2.0.7: resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==} peerDependencies: @@ -12318,18 +12319,19 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-turbo@2.0.4: - resolution: {integrity: sha512-Ozn//vTXJeqIEvEkThM2vuuldMckPqAne7vg/S3GxF+BBY516cjdp7+dYpCU5Q0083hVm638c8542ubccNE+8w==} + eslint-plugin-turbo@2.5.8: + resolution: {integrity: sha512-bVjx4vTH0oTKIyQ7EGFAXnuhZMrKIfu17qlex/dps7eScPnGQLJ3r1/nFq80l8xA+8oYjsSirSQ2tXOKbz3kEw==} peerDependencies: eslint: '>6.6.0' + turbo: '>2.0.0' eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} @@ -12339,19 +12341,23 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + eslint@9.37.0: + resolution: {integrity: sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true esniff@2.0.1: resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} engines: {node: '>=0.10'} - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} @@ -12670,9 +12676,9 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} file-saver@2.0.5: resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} @@ -12741,9 +12747,9 @@ packages: react-dom: optional: true - flat-cache@3.1.0: - resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} - engines: {node: '>=12.0.0'} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} @@ -13050,9 +13056,9 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} @@ -13812,10 +13818,6 @@ packages: resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} engines: {node: '>=12'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-path-inside@4.0.0: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} @@ -14225,8 +14227,8 @@ packages: resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} hasBin: true - keyv@4.5.3: - resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} @@ -15718,10 +15720,6 @@ packages: resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} engines: {node: '>=0.10.0'} - object.values@1.2.0: - resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} - engines: {node: '>= 0.4'} - object.values@1.2.1: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} @@ -18457,9 +18455,6 @@ packages: text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -18804,10 +18799,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -22283,26 +22274,40 @@ snapshots: '@esbuild/win32-x64@0.25.2': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0(supports-color@8.1.1))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))': dependencies: - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.7.0(eslint@8.57.0(supports-color@8.1.1))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))': dependencies: - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint-community/regexpp@4.9.1': {} + '@eslint/config-array@0.21.0(supports-color@8.1.1)': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.0': + dependencies: + '@eslint/core': 0.16.0 - '@eslint/eslintrc@2.1.4(supports-color@8.1.1)': + '@eslint/core@0.16.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1(supports-color@8.1.1)': dependencies: ajv: 6.12.6 debug: 4.4.3(supports-color@8.1.1) - espree: 9.6.1 - globals: 13.24.0 + espree: 10.4.0 + globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -22311,7 +22316,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.0': {} + '@eslint/js@9.37.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.4.0': + dependencies: + '@eslint/core': 0.16.0 + levn: 0.4.1 '@exodus/schemasafe@1.3.0': {} @@ -23079,19 +23091,18 @@ snapshots: dependencies: react-hook-form: 7.47.0(react@18.3.1) - '@humanwhocodes/config-array@0.11.14(supports-color@8.1.1)': + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': dependencies: - '@humanwhocodes/object-schema': 2.0.2 - debug: 4.4.3(supports-color@8.1.1) - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/momoa@2.0.4': {} - '@humanwhocodes/object-schema@2.0.2': {} + '@humanwhocodes/retry@0.4.3': {} '@iconify/types@2.0.0': {} @@ -23766,7 +23777,7 @@ snapshots: '@next/env@15.5.2': {} - '@next/eslint-plugin-next@15.3.1': + '@next/eslint-plugin-next@15.5.4': dependencies: fast-glob: 3.3.1 @@ -24055,7 +24066,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@4.1.2(@types/node@22.13.14)(eslint@8.57.0(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1)': + '@nuxt/vite-builder@4.1.2(@types/node@22.13.14)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1)': dependencies: '@nuxt/kit': 4.1.2(magicast@0.3.5) '@rollup/plugin-replace': 6.0.2(rollup@4.50.2) @@ -24084,7 +24095,7 @@ snapshots: unenv: 2.0.0-rc.21 vite: 7.1.5(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) vite-node: 3.2.4(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - vite-plugin-checker: 0.10.3(eslint@8.57.0(supports-color@8.1.1))(typescript@5.9.2)(vite@7.1.5(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) + vite-plugin-checker: 0.10.3(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(typescript@5.9.2)(vite@7.1.5(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) vue: 3.5.21(typescript@5.9.2) vue-bundle-renderer: 2.1.2 transitivePeerDependencies: @@ -29939,15 +29950,15 @@ snapshots: '@types/zxcvbn@4.4.2': {} - '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/parser': 7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/scope-manager': 8.34.1 - '@typescript-eslint/type-utils': 8.34.1(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - '@typescript-eslint/utils': 8.34.1(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/type-utils': 8.34.1(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.34.1(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/visitor-keys': 8.34.1 - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -29956,14 +29967,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': + '@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@typescript-eslint/scope-manager': 7.2.0 '@typescript-eslint/types': 7.2.0 '@typescript-eslint/typescript-estree': 7.2.0(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: @@ -29992,12 +30003,12 @@ snapshots: dependencies: typescript: 5.9.2 - '@typescript-eslint/type-utils@8.34.1(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': + '@typescript-eslint/type-utils@8.34.1(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@typescript-eslint/typescript-estree': 8.34.1(supports-color@8.1.1)(typescript@5.9.2) - '@typescript-eslint/utils': 8.34.1(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.34.1(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) debug: 4.4.3(supports-color@8.1.1) - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: @@ -30038,13 +30049,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.1(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': + '@typescript-eslint/utils@8.34.1(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.0(supports-color@8.1.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) '@typescript-eslint/scope-manager': 8.34.1 '@typescript-eslint/types': 8.34.1 '@typescript-eslint/typescript-estree': 8.34.1(supports-color@8.1.1)(typescript@5.9.2) - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -30204,24 +30215,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.3(supports-color@8.1.1) - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1) - istanbul-reports: 3.1.7 - magic-string: 0.30.19 - magicast: 0.3.5 - std-env: 3.9.0 - test-exclude: 7.0.1 - tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - '@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9)': dependencies: '@ampproject/remapping': 2.3.0 @@ -30236,7 +30229,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.1) + vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -30315,7 +30308,7 @@ snapshots: sirv: 3.0.0 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.1) + vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) '@vitest/utils@3.0.4': dependencies: @@ -30874,13 +30867,6 @@ snapshots: es-abstract: 1.24.0 es-shim-unscopables: 1.0.2 - array.prototype.flatmap@1.3.2: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.0.2 - array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 @@ -32815,33 +32801,34 @@ snapshots: eslint-barrel-file-utils-win32-ia32-msvc: 0.0.10 eslint-barrel-file-utils-win32-x64-msvc: 0.0.10 - eslint-config-next@15.3.1(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2): + eslint-config-next@15.5.4(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2): dependencies: - '@next/eslint-plugin-next': 15.3.1 + '@next/eslint-plugin-next': 15.5.4 '@rushstack/eslint-patch': 1.10.3 - '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - '@typescript-eslint/parser': 7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - eslint: 8.57.0(supports-color@8.1.1) + '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/parser': 7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) - eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0(supports-color@8.1.1)) - eslint-plugin-react: 7.37.5(eslint@8.57.0(supports-color@8.1.1)) - eslint-plugin-react-hooks: 5.2.0(eslint@8.57.0(supports-color@8.1.1)) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) + eslint-plugin-react: 7.37.5(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: - eslint-import-resolver-webpack - supports-color - eslint-config-prettier@9.1.0(eslint@8.57.0(supports-color@8.1.1)): + eslint-config-prettier@10.1.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): dependencies: - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) - eslint-config-turbo@2.0.4(eslint@8.57.0(supports-color@8.1.1)): + eslint-config-turbo@2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3): dependencies: - eslint: 8.57.0(supports-color@8.1.1) - eslint-plugin-turbo: 2.0.4(eslint@8.57.0(supports-color@8.1.1)) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint-plugin-turbo: 2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3) + turbo: 2.3.3 eslint-import-resolver-node@0.3.9(supports-color@8.1.1): dependencies: @@ -32851,13 +32838,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) enhanced-resolve: 5.18.1 - eslint: 8.57.0(supports-color@8.1.1) - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) fast-glob: 3.3.3 get-tsconfig: 4.10.0 is-core-module: 2.16.1 @@ -32868,74 +32855,63 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - eslint: 8.57.0(supports-color@8.1.1) + '@typescript-eslint/parser': 7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): + eslint-plugin-barrel-files@2.0.7(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): dependencies: - debug: 3.2.7(supports-color@8.1.1) - optionalDependencies: - '@typescript-eslint/parser': 7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) - eslint: 8.57.0(supports-color@8.1.1) - eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.31.0)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - eslint-plugin-barrel-files@2.0.7(eslint@8.57.0(supports-color@8.1.1)): - dependencies: - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) eslint-barrel-file-utils: 0.0.10 requireindex: 1.2.0 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 + array.prototype.flatmap: 1.3.3 debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 minimatch: 3.1.2 object.fromentries: 2.0.8 object.groupby: 1.0.3 - object.values: 1.2.0 + object.values: 1.2.1 semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/parser': 7.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0(supports-color@8.1.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 - array.prototype.flatmap: 1.3.2 + array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 axe-core: 4.10.3 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -32944,11 +32920,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@8.57.0(supports-color@8.1.1)): + eslint-plugin-react-hooks@5.2.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): dependencies: - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) - eslint-plugin-react@7.37.5(eslint@8.57.0(supports-color@8.1.1)): + eslint-plugin-react@7.37.5(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -32956,7 +32932,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -32970,17 +32946,18 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-turbo@2.0.4(eslint@8.57.0(supports-color@8.1.1)): + eslint-plugin-turbo@2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3): dependencies: dotenv: 16.0.3 - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + turbo: 2.3.3 eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 - eslint-scope@7.2.2: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 @@ -32989,46 +32966,45 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@8.57.0(supports-color@8.1.1): + eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0(supports-color@8.1.1)) - '@eslint-community/regexpp': 4.9.1 - '@eslint/eslintrc': 2.1.4(supports-color@8.1.1) - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14(supports-color@8.1.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0(supports-color@8.1.1) + '@eslint/config-helpers': 0.4.0 + '@eslint/core': 0.16.0 + '@eslint/eslintrc': 3.3.1(supports-color@8.1.1) + '@eslint/js': 9.37.0 + '@eslint/plugin-kit': 0.4.0 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.3.7(supports-color@8.1.1) - doctrine: 3.0.0 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.5.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.2.4 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.3 - strip-ansi: 6.0.1 - text-table: 0.2.0 + optionalDependencies: + jiti: 2.5.1 transitivePeerDependencies: - supports-color @@ -33039,11 +33015,11 @@ snapshots: event-emitter: 0.3.5 type: 2.7.3 - espree@9.6.1: + espree@10.4.0: dependencies: acorn: 8.15.0 acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 3.4.3 + eslint-visitor-keys: 4.2.1 esprima@4.0.1: {} @@ -33432,9 +33408,9 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 - file-entry-cache@6.0.1: + file-entry-cache@8.0.0: dependencies: - flat-cache: 3.1.0 + flat-cache: 4.0.1 file-saver@2.0.5: {} @@ -33512,11 +33488,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - flat-cache@3.1.0: + flat-cache@4.0.1: dependencies: flatted: 3.3.2 - keyv: 4.5.3 - rimraf: 3.0.2 + keyv: 4.5.4 flatted@3.3.2: {} @@ -33858,9 +33833,7 @@ snapshots: globals@11.12.0: {} - globals@13.24.0: - dependencies: - type-fest: 0.20.2 + globals@14.0.0: {} globals@15.15.0: {} @@ -34795,8 +34768,6 @@ snapshots: is-obj@3.0.0: {} - is-path-inside@3.0.3: {} - is-path-inside@4.0.0: {} is-plain-obj@4.1.0: {} @@ -35186,7 +35157,7 @@ snapshots: array-includes: 3.1.8 array.prototype.flat: 1.3.2 object.assign: 4.1.7 - object.values: 1.2.0 + object.values: 1.2.1 katex@0.16.21: dependencies: @@ -35196,7 +35167,7 @@ snapshots: dependencies: commander: 8.3.0 - keyv@4.5.3: + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -37448,7 +37419,7 @@ snapshots: next: 15.5.2(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.53.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) react-router: 7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nuxt@4.1.2(@electric-sql/pglite@0.2.15)(@parcel/watcher@2.5.1)(@types/node@22.13.14)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(pg@8.16.3))(encoding@0.1.13)(eslint@8.57.0(supports-color@8.1.1))(ioredis@5.7.0(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@6.3.6(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1): + nuxt@4.1.2(@electric-sql/pglite@0.2.15)(@parcel/watcher@2.5.1)(@types/node@22.13.14)(@vue/compiler-sfc@3.5.21)(aws4fetch@1.0.20)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(pg@8.16.3))(encoding@0.1.13)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(ioredis@5.7.0(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@6.3.6(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.28.0(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -37456,7 +37427,7 @@ snapshots: '@nuxt/kit': 4.1.2(magicast@0.3.5) '@nuxt/schema': 4.1.2 '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 4.1.2(@types/node@22.13.14)(eslint@8.57.0(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1) + '@nuxt/vite-builder': 4.1.2(@types/node@22.13.14)(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(magicast@0.3.5)(rollup@4.50.2)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))(yaml@2.8.1) '@unhead/vue': 2.0.14(vue@3.5.21(typescript@5.9.2)) '@vue/shared': 3.5.21 c12: 3.3.0(magicast@0.3.5) @@ -37658,12 +37629,6 @@ snapshots: dependencies: isobject: 3.0.1 - object.values@1.2.0: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - object.values@1.2.1: dependencies: call-bind: 1.0.8 @@ -41064,8 +41029,6 @@ snapshots: dependencies: b4a: 1.6.7 - text-table@0.2.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -41379,8 +41342,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.20.2: {} - type-fest@0.21.3: {} type-fest@0.7.1: {} @@ -42223,7 +42184,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.10.3(eslint@8.57.0(supports-color@8.1.1))(typescript@5.9.2)(vite@7.1.5(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-plugin-checker@0.10.3(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(typescript@5.9.2)(vite@7.1.5(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -42236,7 +42197,7 @@ snapshots: vite: 7.1.5(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) vscode-uri: 3.1.0 optionalDependencies: - eslint: 8.57.0(supports-color@8.1.1) + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) typescript: 5.9.2 vite-plugin-inspect@11.3.3(@nuxt/kit@3.19.2(magicast@0.3.5))(supports-color@8.1.1)(vite@6.3.6(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)): diff --git a/turbo.json b/turbo.json index 27e175cb07e73..c6d76db1c1f8f 100644 --- a/turbo.json +++ b/turbo.json @@ -67,8 +67,12 @@ "NEXT_PUBLIC_VERCEL_BRANCH_URL", "NEXT_PUBLIC_GOOGLE_MAPS_KEY", "NEXT_RUNTIME", + "NIMBUS_PROD_PROJECTS_URL", + "NIMBUS_PROD_PROJECTS_URL_WS", "NODE_ENV", "SUPABASE_URL", + "VERCEL", + "VERCEL_ENV", // These envs are used in the packages "NEXT_PUBLIC_STORAGE_KEY", "NEXT_PUBLIC_AUTH_DEBUG_KEY", @@ -123,6 +127,7 @@ "dependsOn": ["^build"], "env": [ "ANALYZE", + "CI", "CF_ACCESS_CLIENT_ID", "CF_ACCESS_CLIENT_SECRET", "CMS_API_KEY", @@ -145,6 +150,9 @@ "NEXT_PUBLIC_SENTRY_DSN", "NEXT_PUBLIC_SURVEY_SUPABASE_URL", "NEXT_PUBLIC_SURVEY_SUPABASE_ANON_KEY", + "VERCEL", + "VERCEL_ENV", + "VERCEL_GIT_COMMIT_SHA", // These envs are used in the packages "NEXT_PUBLIC_STORAGE_KEY", "NEXT_PUBLIC_AUTH_DEBUG_KEY", From cab167c15144750516a3faeaeae608b7fff478ab Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:07:24 +0200 Subject: [PATCH 4/4] fix chart headers (#39542) fix --- .../Reports/v2/ReportChartV2.test.tsx | 35 +++++++++++++++++++ .../interfaces/Reports/v2/ReportChartV2.tsx | 30 +++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 apps/studio/components/interfaces/Reports/v2/ReportChartV2.test.tsx diff --git a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.test.tsx b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.test.tsx new file mode 100644 index 0000000000000..85b134bb5e036 --- /dev/null +++ b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.test.tsx @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest' +import { computePeriodTotal } from './ReportChartV2' + +describe('computePeriodTotal', () => { + const attrs = [ + { attribute: 'SignInAttempts', label: 'Password', enabled: true }, + { attribute: 'SignInAttempts', label: 'PKCE', enabled: true }, + { attribute: 'SignInAttempts', label: 'Refresh Token', enabled: true }, + { attribute: 'SignInAttempts', label: 'ID Token', enabled: true }, + ] + + it('deduplicates attributes that map to same field', () => { + const data = [ + { timestamp: 1, SignInAttempts: 1 }, + { timestamp: 2, SignInAttempts: 0 }, + ] + expect(computePeriodTotal(data as any, attrs as any)).toBe(1) + }) + + it('excludes reference lines, max values, omitted and disabled attributes', () => { + const data = [ + { timestamp: 1, a: 1, b: 2, c: 4, d: 8 }, + { timestamp: 2, a: 1, b: 2, c: 4, d: 8 }, + ] + const attributes = [ + { attribute: 'a', enabled: true }, + { attribute: 'b', provider: 'reference-line', enabled: true }, + { attribute: 'c', isMaxValue: true, enabled: true }, + { attribute: 'd', omitFromTotal: true, enabled: true }, + { attribute: 'e', enabled: false }, + ] + // Only 'a' should count: period total = 1+1 = 2 + expect(computePeriodTotal(data as any, attributes as any)).toBe(2) + }) +}) diff --git a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx index 01f9ed5f0040b..b84e320db062f 100644 --- a/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx +++ b/apps/studio/components/interfaces/Reports/v2/ReportChartV2.tsx @@ -26,6 +26,32 @@ export interface ReportChartV2Props { highlightActions?: ChartHighlightAction[] } +// Compute total across entire period over unique attribute keys. +// Excludes attributes that are disabled, reference lines, max values, or marked omitFromTotal. +export function computePeriodTotal(chartData: any[], dynamicAttributes: any[]): number { + const attributeKeys = Array.from( + new Set( + (dynamicAttributes as any[]) + .filter( + (a) => + a?.enabled !== false && + a?.provider !== 'reference-line' && + !a?.isMaxValue && + !a?.omitFromTotal + ) + .map((a: any) => a.attribute) + ) + ) + + return chartData.reduce((sum: number, row: any) => { + const rowTotal = attributeKeys.reduce((acc: number, key: string) => { + const value = row?.[key] + return acc + (typeof value === 'number' ? value : 0) + }, 0) + return sum + rowTotal + }, 0) +} + export const ReportChartV2 = ({ report, projectRef, @@ -72,6 +98,8 @@ export const ReportChartV2 = ({ const chartData = queryResult?.data || [] const dynamicAttributes = queryResult?.attributes || [] + const headerTotal = computePeriodTotal(chartData, dynamicAttributes) + /** * Depending on the source the timestamp key could be 'timestamp' or 'period_start' */ @@ -124,7 +152,7 @@ export const ReportChartV2 = ({ xAxisKey={report.xAxisKey ?? 'timestamp'} yAxisKey={report.yAxisKey ?? dynamicAttributes[0]?.attribute} hideHighlightedValue={report.hideHighlightedValue} - highlightedValue={0} + highlightedValue={headerTotal} title={report.label} customDateFormat={undefined} chartStyle={chartStyle}