diff --git a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx index 43bb41cb0d27b..3888dcf5e7870 100644 --- a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx +++ b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx @@ -70,7 +70,9 @@ export const RateLimits = () => { }, }) - const canUpdateEmailLimit = authConfig?.EXTERNAL_EMAIL_ENABLED && isSmtpEnabled(authConfig) + const canUpdateEmailLimit = + authConfig?.EXTERNAL_EMAIL_ENABLED && + (isSmtpEnabled(authConfig) || authConfig?.HOOK_SEND_EMAIL_ENABLED) const canUpdateSMSRateLimit = authConfig?.EXTERNAL_PHONE_ENABLED const canUpdateAnonymousUsersRateLimit = authConfig?.EXTERNAL_ANONYMOUS_USERS_ENABLED const canUpdateWeb3RateLimit = authConfig?.EXTERNAL_WEB3_SOLANA_ENABLED @@ -271,19 +273,25 @@ export const RateLimits = () => { ) : ( <>

- Custom SMTP provider is required to update this configuration + Custom SMTP or Send Email hook is required to update this + configuration

- The built-in email service has a fixed rate limit. You will need - to set up your own custom SMTP provider to update your email - rate limit + The built-in email service has a fixed rate limit. Set up a + custom SMTP provider or enable the Send Email hook to update + your email rate limit

-
+
+
)} diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts index ca1df29f66097..447cf52485526 100644 --- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts +++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.test.ts @@ -328,7 +328,6 @@ describe('connect.schema:steps resolution', () => { const steps = resolveSteps(connectSchema, state) expect(steps.find((s) => s.id === 'codex-add-server')).toBeDefined() - expect(steps.find((s) => s.id === 'codex-enable-remote')).toBeDefined() expect(steps.find((s) => s.id === 'codex-authenticate')).toBeDefined() expect(steps.find((s) => s.id === 'codex-verify')).toBeDefined() }) @@ -445,9 +444,6 @@ describe('connect.schema:step content paths', () => { expect(steps.find((s) => s.id === 'codex-add-server')?.content).toBe( 'steps/mcp/codex/add-server' ) - expect(steps.find((s) => s.id === 'codex-enable-remote')?.content).toBe( - 'steps/mcp/codex/enable-remote' - ) expect(steps.find((s) => s.id === 'codex-authenticate')?.content).toBe( 'steps/mcp/codex/authenticate' ) diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts index 04200614e2969..6ee7fe32cf1ae 100644 --- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts +++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts @@ -123,13 +123,6 @@ const codexAddServerStep: StepDefinition = { content: 'steps/mcp/codex/add-server', } -const codexEnableRemoteStep: StepDefinition = { - id: 'codex-enable-remote', - title: 'Enable remote MCP client support', - description: 'Add this to your ~/.codex/config.toml file.', - content: 'steps/mcp/codex/enable-remote', -} - const codexAuthenticateStep: StepDefinition = { id: 'codex-authenticate', title: 'Authenticate', @@ -387,13 +380,7 @@ export const connectSchema: ConnectSchema = { orm: [ormInstallStep, ormConfigureStep, skillsInstallStep], mcp: { mcpClient: { - codex: [ - codexAddServerStep, - codexEnableRemoteStep, - codexAuthenticateStep, - codexVerifyStep, - skillsInstallStep, - ], + codex: [codexAddServerStep, codexAuthenticateStep, codexVerifyStep, skillsInstallStep], 'claude-code': [claudeAddServerStep, claudeAuthenticateStep, skillsInstallStep], DEFAULT: [mcpConfigureStep, skillsInstallStep], }, diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/enable-remote/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/enable-remote/content.tsx deleted file mode 100644 index 5f57b5de8a441..0000000000000 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/mcp/codex/enable-remote/content.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { CodeBlock } from 'ui-patterns/CodeBlock' - -import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' - -function CodexEnableRemoteContent(_props: StepContentProps) { - const configContent = `[mcp] -remote_mcp_client_enabled = true` - - return ( - - ) -} - -export default CodexEnableRemoteContent diff --git a/apps/studio/components/interfaces/Database/Schemas/DefaultEdge.tsx b/apps/studio/components/interfaces/Database/Schemas/DefaultEdge.tsx index 692a98db90756..8c17aac26c742 100644 --- a/apps/studio/components/interfaces/Database/Schemas/DefaultEdge.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/DefaultEdge.tsx @@ -8,7 +8,7 @@ import { useReactFlow, } from '@xyflow/react' import { ArrowLeft, ArrowRight } from 'lucide-react' -import { useState } from 'react' +import { memo, useState } from 'react' import { Badge, cn } from 'ui' import { useSchemaGraphContext } from './SchemaGraphContext' @@ -16,7 +16,7 @@ import { EdgeData } from './Schemas.constants' import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' -export const DefaultEdge = ({ +const DefaultEdgeComponent = ({ id, animated, data, @@ -73,6 +73,8 @@ export const DefaultEdge = ({ ) } +export const DefaultEdge = memo(DefaultEdgeComponent) + const EdgeRelationInfo = ({ data, source, diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx index 450ea08914ac1..1632d7323a9ed 100644 --- a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx @@ -187,14 +187,17 @@ export const SchemaGraph = () => { } const selectedNodeIds = new Set(params.nodes.map((n) => n.id)) - reactFlowInstance.setEdges( - reactFlowInstance.getEdges().map((edge) => ({ - ...edge, - animated: - selectedNodeIds.size > 0 && - (selectedNodeIds.has(edge.source) || selectedNodeIds.has(edge.target)), - })) - ) + const currentEdges = reactFlowInstance.getEdges() + let hasChanges = false + const nextEdges = currentEdges.map((edge) => { + const shouldAnimate = + selectedNodeIds.size > 0 && + (selectedNodeIds.has(edge.source) || selectedNodeIds.has(edge.target)) + if (edge.animated === shouldAnimate) return edge + hasChanges = true + return { ...edge, animated: shouldAnimate } + }) + if (hasChanges) reactFlowInstance.setEdges(nextEdges) } ) @@ -253,16 +256,7 @@ export const SchemaGraph = () => { } }) } - }, [ - isSuccessTables, - isSuccessSchemas, - tables, - reactFlowInstance, - ref, - resolvedTheme, - schemas, - selectedSchema, - ]) + }, [isSuccessTables, isSuccessSchemas, tables, reactFlowInstance, ref, schemas, selectedSchema]) const schemaGraphContext = useMemo( () => ({ @@ -470,6 +464,7 @@ export const SchemaGraph = () => { fitView minZoom={0.8} maxZoom={1.8} + onlyRenderVisibleElements proOptions={{ hideAttribution: true }} onNodeDragStop={saveNodePositions} onSelectionChange={handleSelectionChange} diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx index 1d296f1f11635..46b0451df17f8 100644 --- a/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx @@ -13,6 +13,7 @@ import { Table2, } from 'lucide-react' import { useRouter } from 'next/router' +import { memo } from 'react' import { toast } from 'sonner' import { Button, @@ -41,13 +42,15 @@ import { formatSql } from '@/lib/formatSql' export const TABLE_NODE_WIDTH = 320 export const TABLE_NODE_ROW_HEIGHT = 40 -export const TableNode = ({ +type TableNodeOwnProps = NodeProps> & { placeholder?: boolean } + +const TableNodeComponent = ({ id, data, targetPosition, sourcePosition, placeholder, -}: NodeProps> & { placeholder?: boolean }) => { +}: TableNodeOwnProps) => { // Important styles is a nasty hack to use Handles (required for edges calculations), but do not show them in the UI. // ref: https://github.com/wbkd/react-flow/discussions/2698 const hiddenNodeConnector = 'h-px! w-px! min-w-0! min-h-0! cursor-grab! border-0! opacity-0!' @@ -358,3 +361,17 @@ export const TableNode = ({ ) } + +// Custom comparator: xyflow re-renders nodes on selection/drag with new prop +// objects; only the listed props affect rendered output. Selection-driven styling +// (highlighted edges) is read from context inside the component, so we can safely +// ignore xyflow's `selected`/`dragging`/etc. here. +export const TableNode = memo( + TableNodeComponent, + (prev, next) => + prev.id === next.id && + prev.data === next.data && + prev.targetPosition === next.targetPosition && + prev.sourcePosition === next.sourcePosition && + prev.placeholder === next.placeholder +) diff --git a/apps/studio/components/interfaces/Database/Schemas/Schemas.utils.ts b/apps/studio/components/interfaces/Database/Schemas/Schemas.utils.ts index 7089fae809365..b7a72e7825a60 100644 --- a/apps/studio/components/interfaces/Database/Schemas/Schemas.utils.ts +++ b/apps/studio/components/interfaces/Database/Schemas/Schemas.utils.ts @@ -66,6 +66,25 @@ export async function getGraphDataFromTables( 'id' ) + // Precompute name → { tableId, columnsByName } lookup so each relationship + // resolves its source/target handles in O(1) instead of scanning every table+column. + const tablesByName = new Map }>() + for (const table of tables) { + const columnsByName = new Map() + for (const column of table.columns || []) { + columnsByName.set(column.name, column.id) + } + tablesByName.set(table.name, { tableId: table.id, columnsByName }) + } + + const findHandleIds = (tableName: string, columnName: string): [string?, string?] => { + const entry = tablesByName.get(tableName) + if (!entry) return [] + const columnId = entry.columnsByName.get(columnName) + if (columnId === undefined) return [] + return [String(entry.tableId), columnId] + } + for (const rel of uniqueRelationships) { // TODO: Support [external->this] relationship? if (rel.source_schema !== currentSchema) { @@ -96,11 +115,7 @@ export async function getGraphDataFromTables( }) } - const [source, sourceHandle] = findTablesHandleIds( - tables, - rel.source_table_name, - rel.source_column_name - ) + const [source, sourceHandle] = findHandleIds(rel.source_table_name, rel.source_column_name) if (source) { edges.push({ @@ -124,16 +139,8 @@ export async function getGraphDataFromTables( continue } - const [source, sourceHandle] = findTablesHandleIds( - tables, - rel.source_table_name, - rel.source_column_name - ) - const [target, targetHandle] = findTablesHandleIds( - tables, - rel.target_table_name, - rel.target_column_name - ) + const [source, sourceHandle] = findHandleIds(rel.source_table_name, rel.source_column_name) + const [target, targetHandle] = findHandleIds(rel.target_table_name, rel.target_column_name) // We do not support [external->this] flow currently. if (source && target) { @@ -165,24 +172,6 @@ export async function getGraphDataFromTables( : getLayoutedElementsViaDagre(nodes, edges) } -function findTablesHandleIds( - tables: PGTable[], - table_name: string, - column_name: string -): [string?, string?] { - for (const table of tables) { - if (table_name !== table.name) continue - - for (const column of table.columns || []) { - if (column_name !== column.name) continue - - return [String(table.id), column.id] - } - } - - return [] -} - export const getLayoutedElementsViaDagre = (nodes: Node[], edges: Edge[]) => { const dagreGraph = new dagre.graphlib.Graph() dagreGraph.setDefaultEdgeLabel(() => ({})) diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.UpdateSavedQueryModal.tsx b/apps/studio/components/interfaces/Settings/Logs/Logs.UpdateSavedQueryModal.tsx index fecd04e41a287..42f067006a7c0 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.UpdateSavedQueryModal.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.UpdateSavedQueryModal.tsx @@ -1,7 +1,21 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useEffect } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' -import { Button, Form, FormControl, FormField, Input, Modal, Textarea } from 'ui' +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + Form, + FormControl, + FormField, + Input, + Textarea, +} from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' @@ -45,46 +59,56 @@ export const UpdateSavedQueryModal = ({ } return ( - -
- - - ( - - - - - - )} - /> - - - ( - - -