From fa4ce4a9496c8b63fd8bfd52cafce5d03f6efda3 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 25 Oct 2024 09:36:27 +0800 Subject: [PATCH 01/50] Update disk size reduction validation (#30070) * Update disk size reduction validation * Fix * Add docs * Small fix --- .../content/guides/platform/database-size.mdx | 2 + .../DiskManagementPanelForm.tsx | 34 +++- .../DiskManagementPanelSchema.ts | 145 ++++++++++-------- .../InstanceConfiguration.tsx | 2 +- 4 files changed, 110 insertions(+), 73 deletions(-) diff --git a/apps/docs/content/guides/platform/database-size.mdx b/apps/docs/content/guides/platform/database-size.mdx index 3415a649820..3ddc6cb224c 100644 --- a/apps/docs/content/guides/platform/database-size.mdx +++ b/apps/docs/content/guides/platform/database-size.mdx @@ -132,3 +132,5 @@ set default_transaction_read_only = 'off'; ### Reducing disk size Disks don't automatically downsize during normal operation. Once you have [reduced your database size](/docs/guides/platform/database-size#database-size), they _will_ automatically "right-size" during a [project upgrade](/docs/guides/platform/upgrading). The final disk size after the upgrade is 1.2x the size of the database with a minimum of 8 GB. For example, if your database size is 100GB, and you have a 200GB disk, the size after a project upgrade will be 120 GB. + +In the event that your project is already on the latest version of Postgres and cannot be upgraded, a new version of Postgres will be released approximately every week which you can then upgrade to once it becomes available. diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx index 5e58cd95843..e9e82e8e642 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx @@ -1,7 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { AnimatePresence, motion } from 'framer-motion' -import { HelpCircle, InfoIcon, RotateCcw } from 'lucide-react' +import { ExternalLink, HelpCircle, InfoIcon, RotateCcw } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' @@ -60,7 +60,7 @@ import { calculateIOPSPrice, calculateThroughputPrice, } from './DiskManagement.utils' -import { DiskStorageSchema, DiskStorageSchemaType } from './DiskManagementPanelSchema' +import { getDiskStorageSchema, DiskStorageSchemaType } from './DiskManagementPanelSchema' import { DiskManagementPlanUpgradeRequired } from './DiskManagementPlanUpgradeRequired' import { DiskManagementDiskSizeReadReplicas, @@ -68,6 +68,8 @@ import { DiskManagementThroughputReadReplicas, } from './DiskManagementReadReplicas' import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog' +import { Admonition } from 'ui-patterns' +import { Markdown } from '../Markdown' export function DiskManagementPanelForm() { const { project } = useProjectContext() @@ -161,6 +163,7 @@ export function DiskManagementPanelForm() { throughput: throughput_mbps, totalSize: size_gb, } + const DiskStorageSchema = getDiskStorageSchema(size_gb) const form = useForm({ resolver: zodResolver(DiskStorageSchema), defaultValues, @@ -616,8 +619,31 @@ export function DiskManagementPanelForm() { label="Disk Size" layout="horizontal" description={ - includedDiskGB > 0 && - `Your plan includes ${includedDiskGB} GB of disk size for ${watchedStorageType}.` + <> + {includedDiskGB > 0 && + `Your plan includes ${includedDiskGB} GB of disk size for ${watchedStorageType}.`} + {field.value < size_gb && ( + + } + > +
+ +
+
+ )} + } >
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts index ca1db6987fc..37bf8f73fb9 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelSchema.ts @@ -11,89 +11,98 @@ const baseSchema = z.object({ throughput: z.number().optional().describe('Throughput in MB/s for gp3'), }) -export const DiskStorageSchema = baseSchema.superRefine((data, ctx) => { - const { storageType, totalSize, provisionedIOPS, throughput } = data +export const getDiskStorageSchema = (currentDiskSize: number) => + baseSchema.superRefine((data, ctx) => { + const { storageType, totalSize, provisionedIOPS, throughput } = data - if (storageType === 'io2') { - // Validation rules for io2 - const maxIOPS = Math.min(1000 * totalSize, 256000) - if (provisionedIOPS < 100) { + if (totalSize < currentDiskSize) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: 'Provisioned IOPS must be at least 100', - path: ['provisionedIOPS'], - }) - } else if (provisionedIOPS > maxIOPS) { - if (totalSize >= 8) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Provisioned IOPS must be at most ${maxIOPS}.`, - path: ['provisionedIOPS'], - }) - } else { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Invalid IOPS value due to invalid disk size`, - path: ['provisionedIOPS'], - }) - } - } - if (throughput !== undefined) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Throughput is not configurable for io2.', - path: ['throughput'], - }) - } - if (totalSize > DISK_LIMITS[DiskType.IO2].maxStorage) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Allocated disksize must not exceed 61,440 GB', + message: 'Disk size cannot be reduced from current configuration', path: ['totalSize'], }) } - } else if (storageType === 'gp3') { - // Validation rules for gp3 - const maxIOPS = Math.min(500 * totalSize, 16000) - const maxThroughput = Math.min(0.25 * provisionedIOPS, 1000) - if (provisionedIOPS < 3000) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Provisioned IOPS must be at least 3000`, - path: ['provisionedIOPS'], - }) - } else if (provisionedIOPS > maxIOPS) { - if (totalSize >= 8) { + if (storageType === 'io2') { + // Validation rules for io2 + const maxIOPS = Math.min(1000 * totalSize, 256000) + if (provisionedIOPS < 100) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Provisioned IOPS must be at most ${maxIOPS}`, + message: 'Provisioned IOPS must be at least 100', path: ['provisionedIOPS'], }) - } else { + } else if (provisionedIOPS > maxIOPS) { + if (totalSize >= 8) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Provisioned IOPS must be at most ${maxIOPS}.`, + path: ['provisionedIOPS'], + }) + } else { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid IOPS value due to invalid disk size`, + path: ['provisionedIOPS'], + }) + } + } + if (throughput !== undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Invalid IOPS value due to invalid disk size`, + message: 'Throughput is not configurable for io2.', + path: ['throughput'], + }) + } + if (totalSize > DISK_LIMITS[DiskType.IO2].maxStorage) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Allocated disk size must not exceed 61,440 GB', + path: ['totalSize'], + }) + } + } else if (storageType === 'gp3') { + // Validation rules for gp3 + const maxIOPS = Math.min(500 * totalSize, 16000) + const maxThroughput = Math.min(0.25 * provisionedIOPS, 1000) + + if (provisionedIOPS < 3000) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Provisioned IOPS must be at least 3000`, path: ['provisionedIOPS'], }) + } else if (provisionedIOPS > maxIOPS) { + if (totalSize >= 8) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Provisioned IOPS must be at most ${maxIOPS}`, + path: ['provisionedIOPS'], + }) + } else { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid IOPS value due to invalid disk size`, + path: ['provisionedIOPS'], + }) + } + } + + if (throughput !== undefined && (throughput < 125 || throughput > maxThroughput)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Throughput must be set between 125 and ${maxThroughput?.toLocaleString()} MB/s.`, + path: ['throughput'], + }) + } + if (totalSize > DISK_LIMITS[DiskType.GP3].maxStorage) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Allocated disk size must not exceed 16,384 GB', + path: ['totalSize'], + }) } } + }) - if (throughput !== undefined && (throughput < 125 || throughput > maxThroughput)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Throughput must be set between 125 and ${maxThroughput?.toLocaleString()} MB/s.`, - path: ['throughput'], - }) - } - if (totalSize > DISK_LIMITS[DiskType.GP3].maxStorage) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Allocated disksize must not exceed 16,384 GB', - path: ['totalSize'], - }) - } - } -}) - -export type DiskStorageSchemaType = z.infer +export type DiskStorageSchemaType = z.infer> diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx index 81db395954a..6399894149f 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx @@ -207,7 +207,7 @@ const InstanceConfigurationUI = () => { }, [isSuccessReplicas, isSuccessLoadBalancers, nodes, edges, view]) return ( -
+
Date: Fri, 25 Oct 2024 09:57:09 +0800 Subject: [PATCH 02/50] Chore/assistant tweaks (#30074) * Add cmd i to close the assistant * Update command in side nav * Add kill switch for sending assistant prompts * Add basic telemetry around quick sql editor assistant * Fix --- .../NavigationBar/CommandOption.tsx | 45 +++++++++++ .../NavigationBar/NavigationBar.tsx | 75 +++++++++++-------- .../ui/AIAssistantPanel/AIAssistant.tsx | 39 ++++++++-- .../ui/AIAssistantPanel/AIAssistantPanel.tsx | 26 ++++++- .../components/ui/CodeEditor/CodeEditor.tsx | 14 +++- apps/studio/lib/constants/index.ts | 19 +++++ 6 files changed, 177 insertions(+), 41 deletions(-) create mode 100644 apps/studio/components/layouts/ProjectLayout/NavigationBar/CommandOption.tsx diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/CommandOption.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/CommandOption.tsx new file mode 100644 index 00000000000..5e56cc96511 --- /dev/null +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/CommandOption.tsx @@ -0,0 +1,45 @@ +import { Command } from 'lucide-react' +import { ReactNode } from 'react' + +import { detectOS } from 'lib/helpers' +import { cn } from 'ui' + +interface CommandOptionProps { + icon: ReactNode + label: string + shortcut: string + onClick: () => void +} + +export const CommandOption = ({ icon, label, shortcut, onClick }: CommandOptionProps) => { + const os = detectOS() + + return ( +
+
+ {icon} +

{label}

+
+
+ {/* Issue with `os` and hydration fail */} + {os === 'macos' || true ? ( + + ) : ( +

CTRL

+ )} +

{shortcut}

+
+
+ ) +} diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 645e52260ef..5e1785de0c0 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -16,6 +16,7 @@ import { detectOS } from 'lib/helpers' import { useProfile } from 'lib/profile' import { useAppStateSnapshot } from 'state/app-state' import { + AiIconAnimation, Button, DropdownMenu, DropdownMenuContent, @@ -26,6 +27,9 @@ import { DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, + HoverCardContent_Shadcn_, + HoverCardTrigger_Shadcn_, + HoverCard_Shadcn_, Separator, Theme, cn, @@ -44,12 +48,12 @@ import NavigationIconLink from './NavigationIconLink' import { useProjectLintsQuery } from 'data/lint/lint-query' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui' +import { CommandOption } from './CommandOption' export const ICON_SIZE = 20 export const ICON_STROKE_WIDTH = 1.5 const NavigationBar = () => { - const os = detectOS() const router = useRouter() const { profile } = useProfile() const { project } = useProjectContext() @@ -108,36 +112,41 @@ const NavigationBar = () => { ) } - const SearchButton = ( - { - setCommandMenuOpen(true) - snap.setNavigationPanelOpen(false) - }} - type="text" - icon={} - rightText={ -
+ + } > - {os === 'macos' || true ? ( // todo: issue with `os` and hydration fail - - ) : ( -

CTRL

- )} -

K

-
- } - > - Search -
+ Commands + + + + + +
+ } + label="Search" + shortcut="K" + onClick={() => { + setCommandMenuOpen(true) + snap.setNavigationPanelOpen(false) + }} + /> + + } + label="Assistant" + shortcut="I" + onClick={() => snap.setAiAssistantPanel({ open: true, editor: null })} + /> + + ) const UserAccountButton = ( @@ -312,13 +321,13 @@ const NavigationBar = () => { <> {!allowNavPanelToExpand && ( - {SearchButton} + {CommandButton} - Search + Commands )} - {allowNavPanelToExpand && SearchButton} + {allowNavPanelToExpand && CommandButton} )} diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index 90a008b41b0..fcbf8d16652 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -15,17 +15,24 @@ import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types' import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation' import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi' import { useSchemasForAi } from 'hooks/misc/useSchemasForAi' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useSelectedProject } from 'hooks/misc/useSelectedProject' -import { BASE_PATH, IS_PLATFORM, OPT_IN_TAGS } from 'lib/constants' +import { useFlag } from 'hooks/ui/useFlag' +import { + BASE_PATH, + IS_PLATFORM, + OPT_IN_TAGS, + TELEMETRY_ACTIONS, + TELEMETRY_CATEGORIES, + TELEMETRY_LABELS, +} from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { AiIconAnimation, - Alert_Shadcn_, - AlertDescription_Shadcn_, Button, cn, DropdownMenu, @@ -79,6 +86,7 @@ export const AIAssistant = ({ const selectedOrganization = useSelectedOrganization() const includeSchemaMetadata = isOptedInToAI || !IS_PLATFORM + const disablePrompts = useFlag('disableAssistantPrompts') const { aiAssistantPanel } = useAppStateSnapshot() const { editor } = aiAssistantPanel @@ -127,6 +135,15 @@ export const AIAssistant = ({ .map((def) => def.sql.trim()) : undefined + const { mutate: sendEvent } = useSendEventMutation() + const sendTelemetryEvent = (action: string) => { + sendEvent({ + action, + category: TELEMETRY_CATEGORIES.AI_ASSISTANT, + label: TELEMETRY_LABELS.QUICK_SQL_EDITOR, + }) + } + const { messages: chatMessages, isLoading: isChatLoading, @@ -172,6 +189,7 @@ export const AIAssistant = ({ append(payload) setAssistantError(undefined) setLastSentMessage(payload) + sendTelemetryEvent(TELEMETRY_ACTIONS.PROMPT_SUBMITTED) } const toggleSchema = (schema: string) => { @@ -180,6 +198,7 @@ export const AIAssistant = ({ } else { const newSelectedSchemas = [...selectedSchemas, schema].sort((a, b) => a.localeCompare(b)) setSelectedSchemas(newSelectedSchemas) + sendTelemetryEvent(TELEMETRY_ACTIONS.SCHEMA_CONTEXT_ADDED) } } @@ -192,6 +211,7 @@ export const AIAssistant = ({ (a, b) => a.schema.localeCompare(b.schema) || a.name.localeCompare(b.name) ) setSelectedTables(newselectedTables) + sendTelemetryEvent(TELEMETRY_ACTIONS.TABLE_CONTEXT_ADDED) } } @@ -205,6 +225,7 @@ export const AIAssistant = ({ if (prompt) { setValue(prompt) sendMessageToAssistant(prompt) + sendTelemetryEvent(TELEMETRY_ACTIONS.QUICK_PROMPT_SELECTED(type)) } } @@ -420,6 +441,13 @@ export const AIAssistant = ({ )}
+ {disablePrompts && ( + + )}
@@ -429,7 +457,7 @@ export const AIAssistant = ({ @@ -582,7 +610,7 @@ export const AIAssistant = ({ '[&>textarea]:rounded-none [&>textarea]:border-0 [&>textarea]:!outline-none [&>textarea]:!ring-offset-0 [&>textarea]:!ring-0' )} loading={isLoading} - disabled={isLoading} + disabled={disablePrompts || isLoading} placeholder={ hasMessages ? 'Reply to the assistant...' : 'How can we help you today?' } @@ -594,6 +622,7 @@ export const AIAssistant = ({ }} />
+ {!hasMessages && ( { const os = detectOS() @@ -80,6 +87,15 @@ export const AiAssistantPanel = () => { (error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? [] const entityContext = ASSISTANT_SUPPORT_ENTITIES.find((x) => x.id === editor) + const { mutate: sendEvent } = useSendEventMutation() + const sendTelemetryEvent = (action: string) => { + sendEvent({ + action, + category: TELEMETRY_CATEGORIES.AI_ASSISTANT, + label: TELEMETRY_LABELS.QUICK_SQL_EDITOR, + }) + } + const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({ onSuccess: async (res) => { // [Joshen] If in a specific editor context mode, assume that intent was to create/update @@ -156,6 +172,7 @@ export const AiAssistantPanel = () => { }) setDebugThread([...debugThread, assistantMessageBefore]) + sendTelemetryEvent(TELEMETRY_ACTIONS.FIX_WITH_ASSISTANT) try { const { solution, sql } = await debugSql({ @@ -190,6 +207,7 @@ export const AiAssistantPanel = () => { ${value} `.trim(), }) + sendTelemetryEvent(TELEMETRY_ACTIONS.EXPLAIN_CODE) } const updateEditorWithCheckForDiff = useCallback( @@ -334,6 +352,10 @@ export const AiAssistantPanel = () => { actions={{ runQuery: { enabled: true, callback: () => onExecuteSql() }, explainCode: { enabled: true, callback: onExplainSql }, + closeAssistant: { + enabled: true, + callback: () => setAiAssistantPanel({ open: false }), + }, }} />
diff --git a/apps/studio/components/ui/CodeEditor/CodeEditor.tsx b/apps/studio/components/ui/CodeEditor/CodeEditor.tsx index 62c08d73b6b..547bab47a34 100644 --- a/apps/studio/components/ui/CodeEditor/CodeEditor.tsx +++ b/apps/studio/components/ui/CodeEditor/CodeEditor.tsx @@ -10,6 +10,7 @@ import { Loading } from '../Loading' import { alignEditor } from './CodeEditor.utils' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { formatQuery } from 'data/sql/format-sql-query' +import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' type CodeEditorActions = { enabled: boolean; callback: (value: any) => void } const DEFAULT_ACTIONS = { @@ -17,6 +18,7 @@ const DEFAULT_ACTIONS = { explainCode: { enabled: false, callback: noop }, formatDocument: { enabled: true, callback: noop }, placeholderFill: { enabled: true }, + closeAssistant: { enabled: false, callback: noop }, } interface CodeEditorProps { @@ -37,6 +39,7 @@ interface CodeEditorProps { formatDocument: CodeEditorActions placeholderFill: Omit explainCode: CodeEditorActions + closeAssistant: CodeEditorActions }> editorRef?: MutableRefObject onInputChange?: (value?: string) => void @@ -66,7 +69,7 @@ const CodeEditor = ({ const editorRef = editorRefProps || ref const monacoRef = useRef() - const { runQuery, placeholderFill, formatDocument, explainCode } = { + const { runQuery, placeholderFill, formatDocument, explainCode, closeAssistant } = { ...DEFAULT_ACTIONS, ...actions, } @@ -168,6 +171,15 @@ const CodeEditor = ({ }) } + if (closeAssistant.enabled) { + editor.addAction({ + id: 'close-assistant', + label: 'Close Assistant', + keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.KeyI], + run: () => closeAssistant.callback(), + }) + } + const model = editor.getModel() if (model) { const position = model.getPositionAt((value ?? '').length) diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts index c1e359bda6f..d7e7533b55f 100644 --- a/apps/studio/lib/constants/index.ts +++ b/apps/studio/lib/constants/index.ts @@ -86,3 +86,22 @@ export const OPT_IN_TAGS = { export const GB = 1024 * 1024 * 1024 export const MB = 1024 * 1024 export const KB = 1024 + +// [Joshen] Just adding these to start consolidating our telemetry configs +// may change depending on how we choose to standardize across all apps +export enum TELEMETRY_CATEGORIES { + AI_ASSISTANT = 'ai-assistant', +} + +export enum TELEMETRY_LABELS { + QUICK_SQL_EDITOR = 'quick-sql-editor', +} + +export const TELEMETRY_ACTIONS = { + PROMPT_SUBMITTED: 'prompt-submitted', + QUICK_PROMPT_SELECTED: (type: string) => `quick-prompt-selected-${type}`, + SCHEMA_CONTEXT_ADDED: 'schema-context-added', + TABLE_CONTEXT_ADDED: 'table-context-added', + FIX_WITH_ASSISTANT: 'fix-with-assistant', + EXPLAIN_CODE: 'explain-code', +} From d1d029747f2e5da5b94fbcbb66280e0f0d2086cb Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 25 Oct 2024 11:26:50 +0800 Subject: [PATCH 03/50] Ensure commands only show up if assistant is enabled (#30088) --- .../NavigationBar/NavigationBar.tsx | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 5e1785de0c0..d25207c48a8 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -7,8 +7,13 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' -import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { + useIsAPIDocsSidePanelEnabled, + useIsDatabaseFunctionsAssistantEnabled, +} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { useProjectLintsQuery } from 'data/lint/lint-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useFlag } from 'hooks/ui/useFlag' import { useSignOut } from 'lib/auth' import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' @@ -32,11 +37,15 @@ import { HoverCard_Shadcn_, Separator, Theme, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, + Tooltip_Shadcn_, cn, singleThemes, } from 'ui' import { useSetCommandMenuOpen } from 'ui-patterns/CommandMenu' import { useProjectContext } from '../ProjectContext' +import { CommandOption } from './CommandOption' import { generateOtherRoutes, generateProductRoutes, @@ -45,15 +54,12 @@ import { } from './NavigationBar.utils' import { NavigationIconButton } from './NavigationIconButton' import NavigationIconLink from './NavigationIconLink' -import { useProjectLintsQuery } from 'data/lint/lint-query' -import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -import { Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui' -import { CommandOption } from './CommandOption' export const ICON_SIZE = 20 export const ICON_STROKE_WIDTH = 1.5 const NavigationBar = () => { + const os = detectOS() const router = useRouter() const { profile } = useProfile() const { project } = useProjectContext() @@ -66,6 +72,7 @@ const NavigationBar = () => { const navLayoutV2 = useFlag('navigationLayoutV2') const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled() + const isFunctionsAssistantEnabled = useIsDatabaseFunctionsAssistantEnabled() const [userDropdownOpen, setUserDropdownOpenState] = useState(false) const [allowNavPanelToExpand] = useLocalStorageQuery( @@ -112,7 +119,37 @@ const NavigationBar = () => { ) } - const CommandButton = ( + const CommandButton = isFunctionsAssistantEnabled ? ( + { + setCommandMenuOpen(true) + snap.setNavigationPanelOpen(false) + }} + type="text" + icon={} + rightText={ +
+ {os === 'macos' || true ? ( // todo: issue with `os` and hydration fail + + ) : ( +

CTRL

+ )} +

K

+
+ } + > + Search +
+ ) : ( Date: Fri, 25 Oct 2024 15:30:41 +0800 Subject: [PATCH 04/50] Tiny bumps to cron (#30089) --- .../components/interfaces/Integrations/CronJobs/CronJobCard.tsx | 2 +- .../interfaces/Integrations/CronJobs/CronJobsListing.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx index fd6e123e6eb..fd09c29cd3c 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx @@ -49,7 +49,7 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard <>
- +
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx index 24a6b8958ba..ab5374ba74e 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx @@ -66,7 +66,7 @@ export const CronJobsListing = () => {
diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx index a7a08d5f617..e9deeeb9190 100644 --- a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx +++ b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx @@ -24,7 +24,7 @@ const ProjectIntegrationsMenu = () => { projectRef: project?.ref, connectionString: project?.connectionString, }) - console.log({ data }) + const pgNetExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_net') !== undefined const graphqlExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_graphql') !== undefined From b6647d445c7983a61f5a87fe813a3bbd4171ecd0 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:40:14 +0200 Subject: [PATCH 06/50] store selected logs to url (#29802) * add selected logs to url * Add mocks for nuqs package. * fix layout issues n remove magic numbers for height * fix chart height issue * fix bug where logs not in first page wouldnt load correctly * fix spacing between charts and header * fix postgtres_logs issues, improve size in log detail * fix borders * fix default renderer * fix detail width * rm maxheight references * fix tests * fix typeerrs * fix stories typerr * improve colelctions empty state * move state UP for logs and warehouse tables * cleanup * rm params * rm params * rm params * fix log explorer selection * fix tests types * cleanup divs * fix selected log delay * use isEq for comparison in rows * fix selection stuck open * fix btnlink combo --------- Co-authored-by: Ivan Vasilov --- .../WarehouseCollectionDetail.tsx | 45 ++-- .../interfaces/Settings/Logs/LogSelection.tsx | 233 +++++------------- .../WarehouseSelectionRenderer.tsx | 2 - .../Settings/Logs/LogTable.stories.tsx | 1 - .../interfaces/Settings/Logs/LogTable.tsx | 97 +++++--- .../Settings/Logs/LogsPreviewer.tsx | 43 ++-- .../Settings/Logs/PreviewFilterPanel.tsx | 4 +- .../layouts/LogsLayout/LogsSidebarMenuV2.tsx | 21 +- .../warehouse-collection-log-detail-query.ts | 19 ++ .../studio/hooks/analytics/useLogsPreview.tsx | 18 +- apps/studio/hooks/analytics/useSelectedLog.ts | 6 + apps/studio/hooks/analytics/useSingleLog.tsx | 27 +- .../project/[ref]/logs/explorer/index.tsx | 10 +- .../project/[ref]/logs/postgres-logs.tsx | 4 +- .../tests/pages/projects/LogTable.test.tsx | 34 +-- .../tests/pages/projects/logs-query.test.tsx | 6 + 16 files changed, 274 insertions(+), 296 deletions(-) create mode 100644 apps/studio/data/analytics/warehouse-collection-log-detail-query.ts create mode 100644 apps/studio/hooks/analytics/useSelectedLog.ts diff --git a/apps/studio/components/interfaces/DataWarehouse/WarehouseCollectionDetail.tsx b/apps/studio/components/interfaces/DataWarehouse/WarehouseCollectionDetail.tsx index 5bd3d6a413e..dd6032a6ece 100644 --- a/apps/studio/components/interfaces/DataWarehouse/WarehouseCollectionDetail.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/WarehouseCollectionDetail.tsx @@ -19,6 +19,8 @@ import DatePickers from '../Settings/Logs/Logs.DatePickers' import { DatetimeHelper } from '../Settings/Logs/Logs.types' import dayjs from 'dayjs' import { useKeyboardShortcuts } from 'components/grid/components/common/Hooks' +import { useWarehouseLogDetailQuery } from 'data/analytics/warehouse-collection-log-detail-query' +import { useSelectedLog } from 'hooks/analytics/useSelectedLog' const INTERVALS: DatetimeHelper[] = [ { @@ -154,6 +156,19 @@ order by timestamp desc limit ${filters.limit} offset ${filters.offset} ) } + + // Selected Log Detail + const [selectedLogId, setSelectedLogId] = useSelectedLog() + const { + data: selectedLogDetail, + isLoading: selectedLogDetailLoading, + error: selectedLogDetailError, + } = useWarehouseLogDetailQuery({ + ref: projectRef, + collectionName: collection?.name, + logId: selectedLogId ?? undefined, + }) + return (
@@ -248,9 +263,11 @@ order by timestamp desc limit ${filters.limit} offset ${filters.offset} hasEditorValue={false} projectRef={projectRef} isLoading={isLoading} + onSelectedLogChange={(log) => setSelectedLogId(log?.id ?? null)} + selectedLog={selectedLogDetail?.result[0] ?? undefined} + selectedLogError={selectedLogDetailError ?? undefined} + isSelectedLogLoading={!!selectedLogId && selectedLogDetailLoading} data={results} - params={{ sql }} - maxHeight="calc(100vh - 139px)" showHeader={false} EmptyState={ @@ -277,20 +294,18 @@ order by timestamp desc limit ${filters.limit} offset ${filters.offset} {!isError && ( -
-
+
+
{results.length > 0 && ( - <> - - + )} {filters.offset !== 0 && ( <> diff --git a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx index 9fb31ffccdf..fb62e0e5360 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx @@ -1,10 +1,4 @@ import { MousePointerClick, X } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' -import { toast } from 'sonner' - -import { Loading } from 'components/ui/Loading' -import { useWarehouseQueryQuery } from 'data/analytics/warehouse-query' -import useSingleLog from 'hooks/analytics/useSingleLog' import { Button, CodeBlock, @@ -17,197 +11,60 @@ import { import AuthSelectionRenderer from './LogSelectionRenderers/AuthSelectionRenderer' import DatabaseApiSelectionRender from './LogSelectionRenderers/DatabaseApiSelectionRender' import DatabasePostgresSelectionRender from './LogSelectionRenderers/DatabasePostgresSelectionRender' -import DefaultExplorerSelectionRenderer from './LogSelectionRenderers/DefaultExplorerSelectionRenderer' import DefaultPreviewSelectionRenderer from './LogSelectionRenderers/DefaultPreviewSelectionRenderer' import FunctionInvocationSelectionRender from './LogSelectionRenderers/FunctionInvocationSelectionRender' import FunctionLogsSelectionRender from './LogSelectionRenderers/FunctionLogsSelectionRender' -import type { LogData, LogsEndpointParams, QueryType } from './Logs.types' -import { isDefaultLogPreviewFormat, isUnixMicro, unixMicroToIsoTimestamp } from './Logs.utils' +import type { LogData, QueryType } from './Logs.types' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import { WarehouseSelectionRenderer } from './LogSelectionRenderers/WarehouseSelectionRenderer' export interface LogSelectionProps { - log: LogData | null + log?: LogData onClose: () => void queryType?: QueryType projectRef: string - params: Partial collectionName?: string + isLoading: boolean + error?: string | object } -const LogSelection = ({ - projectRef, - log: partialLog, - onClose, - queryType, - params = {}, - collectionName, -}: LogSelectionProps) => { - const { logData: fullLog, isLoading } = useSingleLog( - projectRef, - queryType, - params, - partialLog?.id - ) - const [sql, setSql] = useState('') +const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectionProps) => { + const LogDetails = () => { + if (error) return + if (!log) return + // if (!log.metadata) return - const warehouseQueryEnabled = queryType === 'warehouse' - const { - refetch: refetchWarehouseData, - data: warehouseQueryData, - isLoading: warehouseQueryLoading, - } = useWarehouseQueryQuery( - { - ref: projectRef, - sql, - }, - { - enabled: queryType === 'warehouse', - onError: (error) => { - toast.error(error.message) - }, - } - ) - - useEffect(() => { - const newSql = `select id, timestamp, event_message, metadata from \`${collectionName}\` - where id = '${partialLog?.id}' and timestamp > '2024-01-01' limit 1` - - setSql(newSql) - }, [collectionName, partialLog?.id]) - - useEffect(() => { - if (queryType === 'warehouse') { - refetchWarehouseData() - } - }, [ - warehouseQueryData, - collectionName, - projectRef, - partialLog?.id, - refetchWarehouseData, - queryType, - ]) - - const Formatter = () => { switch (queryType) { case 'warehouse': - if (!warehouseQueryData) return null - return + return case 'api': - if (!fullLog) return null - if (!fullLog.metadata) return - return + return case 'database': - if (!fullLog) return null - if (!fullLog.metadata) return - return + return case 'fn_edge': - if (!fullLog) return null - if (!fullLog.metadata) return - return + return case 'functions': - if (!fullLog) return null - if (!fullLog.metadata) return - return + return case 'auth': - if (!fullLog) return null - if (!fullLog.metadata) return - return - + return default: - if (queryType && fullLog && isDefaultLogPreviewFormat(fullLog)) { - return - } - if (queryType && !fullLog) { - return null - } - if (!partialLog) return null - return + return } } - const selectionText = useMemo(() => { - if (fullLog && queryType) { - return `Log ID - ${fullLog.id}\n - Log Timestamp (UTC) - ${isUnixMicro(fullLog.timestamp) ? unixMicroToIsoTimestamp(fullLog.timestamp) : fullLog.timestamp}\n - Log Event Message - ${fullLog.event_message}\n - Log Metadata - ${JSON.stringify(fullLog.metadata, null, 2)} - ` - } - - return JSON.stringify(fullLog || partialLog, null, 2) - }, [fullLog, partialLog, queryType]) - - const rawLog = useMemo(() => { - if (queryType === 'warehouse') { - return warehouseQueryData?.result?.[0] || {} - } - return fullLog || partialLog - }, [queryType, warehouseQueryData, fullLog, partialLog]) - return ( -
-
-
-
-
-
- -
-
-
-

Select an Event

-

- {queryType === 'warehouse' - ? 'Select an Event to view the complete JSON payload' - : 'Select an Event to view the code snippet (pretty view) or complete JSON payload (raw view).'} -

-
-
-
-
- +
+
+ Details - + Raw -
- {isLoading || (warehouseQueryEnabled && warehouseQueryLoading) ? ( +
+ {isLoading ? (
) : ( <> - - + + - {isLoading && 'Loading...'} - {JSON.stringify(rawLog, null, 2)} + {JSON.stringify(log, null, 2)} @@ -248,3 +104,40 @@ const LogSelection = ({ } export default LogSelection + +function LogDetailEmptyState({ + title = 'Select an Event', + message = 'Select an Event to view the complete JSON payload', +}: { + title?: string + message?: string +}) { + return ( +
+
+
+
+
+ +
+
+
+

{title}

+

{message}

+
+
+
+ ) +} + +function LogErrorState({ error }: { error?: string | object }) { + return
{JSON.stringify(error, null, 2)}
+} diff --git a/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/WarehouseSelectionRenderer.tsx b/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/WarehouseSelectionRenderer.tsx index 330645b7b13..43047d1882e 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/WarehouseSelectionRenderer.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/WarehouseSelectionRenderer.tsx @@ -33,8 +33,6 @@ const DetailedJsonRow = ({ export const WarehouseSelectionRenderer = ({ log }: any) => { return (
-
{JSON.stringify(log, null, 2)}
-
{Object.entries(log).map(([key, value], index) => { return (
(
+ data?: LogData[] onHistogramToggle?: () => void isHistogramShowing?: boolean isLoading?: boolean @@ -46,17 +47,19 @@ interface Props { showDownload?: boolean queryType?: QueryType projectRef: string - params: LogSelectionProps['params'] onRun?: () => void onSave?: () => void hasEditorValue?: boolean - maxHeight?: string className?: string collectionName?: string // Used for warehouse queries warehouseError?: string EmptyState?: ReactNode showHeader?: boolean showHistogramToggle?: boolean + selectedLog?: LogData + isSelectedLogLoading?: boolean + selectedLogError?: LogQueryError | ResponseError + onSelectedLogChange?: (log: LogData | null) => void } type LogMap = { [id: string]: LogData } @@ -73,35 +76,44 @@ const LogTable = ({ isLoading, error, projectRef, - params, onRun, onSave, hasEditorValue, - maxHeight, className, collectionName, EmptyState, showHeader = true, showHistogramToggle = true, + selectedLog, + isSelectedLogLoading, + selectedLogError, + onSelectedLogChange, }: Props) => { const { profile } = useProfile() const { show: showContextMenu } = useContextMenu() const [cellPosition, setCellPosition] = useState() - const [focusedLog, setFocusedLog] = useState(null) + + const [selectionOpen, setSelectionOpen] = useState(false) + + useEffect(() => { + if (selectedLog || isSelectedLogLoading) { + setSelectionOpen(true) + } + }, [selectedLog, isSelectedLogLoading]) const canCreateLogQuery = useCheckPermissions(PermissionAction.CREATE, 'user_content', { resource: { type: 'log_sql', owner_id: profile?.id }, subject: { id: profile?.id }, }) - const firstRow: LogData | undefined = data?.[0] as LogData + const firstRow = data[0] // move timestamp to the first column, if it exists function getFirstRow() { if (!firstRow) return {} - const { timestamp, ...rest } = firstRow || {} + const { timestamp, ...rest } = firstRow if (!timestamp) return firstRow @@ -172,31 +184,22 @@ const LogTable = ({ } } - const stringData = JSON.stringify(data) + const stringData = useMemo(() => JSON.stringify(data), [data]) const [dedupedData, logMap] = useMemo<[LogData[], LogMap]>(() => { const deduped = [...new Set(data)] as LogData[] if (!hasId) { - return [deduped, {} as LogMap] + return [deduped, {}] } const map = deduped.reduce((acc: LogMap, d: LogData) => { acc[d.id] = d return acc - }, {}) as LogMap + }, {}) return [deduped, map] }, [data, hasId]) - useEffect(() => { - if (!data) return - const found = data.find((datum) => isEqual(datum, focusedLog)) - if (!found) { - // close selection panel if not found in dataset - setFocusedLog(null) - } - }, [data, focusedLog, stringData]) - const logDataRows = useMemo(() => { if (hasId && hasTimestamp) { return Object.values(logMap).sort((a, b) => b.timestamp - a.timestamp) @@ -346,37 +349,44 @@ const LogTable = ({ else return } - if (!data) return null + const [selectedRow, setSelectedRow] = useState(null) + function onRowClick(row: LogData) { + setSelectedRow(row) + onSelectedLogChange?.(row) + } + + if (!data) return null return ( -
+
{!queryType && } - + { - setFocusedLog(row.row as LogData) setCellPosition(row) }} - selectedRows={new Set([])} + onCellClick={(row) => { + onRowClick(row.row) + }} columns={columns} - rowClass={(row: LogData) => - [ - 'font-mono tracking-tight', - isEqual(row, focusedLog) - ? '!bg-surface-300 rdg-row--focused' - : ' !bg-studio hover:!bg-surface-100 cursor-pointer', - ].join(' ') - } + rowClass={(row: LogData) => { + return cn( + 'font-mono tracking-tight !bg-studio hover:!bg-surface-100 cursor-pointer', + { + '!bg-surface-200 rdg-row--focused': isEqual(row, selectedRow), + } + ) + }} rows={logDataRows} rowKeyGetter={(r) => { if (!hasId) return JSON.stringify(r) @@ -404,15 +414,20 @@ const LogTable = ({ document.body )} - - {focusedLog && ( - + + + {selectionOpen && ( + setFocusedLog(null)} - log={focusedLog} + onClose={() => { + onSelectedLogChange?.(null) + setSelectionOpen(false) + }} + log={selectedLog} + error={selectedLogError} queryType={queryType} - params={params} collectionName={collectionName} /> diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index 8eddcc8d4a8..ee03bbd0c93 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -19,6 +19,8 @@ import { LOGS_TABLES, LOG_ROUTES_WITH_REPLICA_SUPPORT, LogsTableName } from './L import type { Filters, LogSearchCallback, LogTemplate, QueryType } from './Logs.types' import { ensureNoTimestampConflict, maybeShowUpgradePrompt } from './Logs.utils' import UpgradePrompt from './UpgradePrompt' +import { useSelectedLog } from 'hooks/analytics/useSelectedLog' +import useSingleLog from 'hooks/analytics/useSingleLog' /** * Acts as a container component for the entire log display @@ -73,6 +75,18 @@ export const LogsPreviewer = ({ setParams, } = useLogsPreview({ projectRef, table, filterOverride }) + const [selectedLogId, setSelectedLogId] = useSelectedLog() + const { + data: selectedLog, + isLoading: isSelectedLogLoading, + error: selectedLogError, + } = useSingleLog({ + projectRef, + id: selectedLogId ?? undefined, + queryType, + paramsToMerge: params, + }) + const { showUpgradePrompt, setShowUpgradePrompt } = useUpgradePrompt( params.iso_timestamp_start as string ) @@ -182,15 +196,8 @@ export const LogsPreviewer = ({ } } - const noResults = logData.length === 0 && !error - const footerHeight = noResults ? '0px' : '48px' - const navBarHeight = '48px' - const filterPanelHeight = '58px' - const chartHeight = showChart && !noResults ? '114px' : '0px' - const maxHeight = `calc(100vh - ${navBarHeight} - ${filterPanelHeight} - ${footerHeight} - ${chartHeight})` - return ( -
+
0 ? 'mb-4 h-24 pt-4 opacity-100' : 'h-0 opacity-0') + (showChart && logData.length > 0 ? 'mb-4 h-28 opacity-100' : 'h-0 opacity-0') } > -
+
{showChart && (
-
+
setShowChart(!showChart)} - params={params} error={error} EmptyState={EmptyState} + onSelectedLogChange={(log) => setSelectedLogId(log?.id ?? null)} + selectedLog={selectedLog} + isSelectedLogLoading={isSelectedLogLoading} + selectedLogError={selectedLogError ?? undefined} />
{!error && logData.length > 0 && ( -
+
+
+ Showing {logData.length} results +
diff --git a/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx index 04be1d56724..ffe90c561d0 100644 --- a/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx @@ -121,9 +121,7 @@ const PreviewFilterPanel = ({ const handleInputSearch = (query: string) => onSearch('search-input-change', { query }) return ( -
+