Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion apps/studio/components/grid/components/grid/ColumnHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { XYCoord } from 'dnd-core'
import { ArrowRight, Key, Link, Lock } from 'lucide-react'
import { ArrowRight, Key, Link, Lock, Lightbulb } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { useDrag, useDrop } from 'react-dnd'

import { getForeignKeyCascadeAction } from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import {
useColumnHasIndexSuggestion,
useTableIndexAdvisor,
} from '../../context/TableIndexAdvisorContext'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import type { ColumnHeaderProps, ColumnType, DragItem, GridForeignKey } from '../../types'
Expand All @@ -24,6 +28,8 @@ export function ColumnHeader<R>({
const columnFormat = getColumnFormat(columnType, format)
const snap = useTableEditorTableStateSnapshot()
const hoverValue = column.name as string
const hasIndexSuggestion = useColumnHasIndexSuggestion(column.name as string)
const { openSheet } = useTableIndexAdvisor()

// keep snap.gridColumns' order in sync with data grid component
useEffect(() => {
Expand Down Expand Up @@ -144,6 +150,21 @@ export function ColumnHeader<R>({
</TooltipContent>
</Tooltip>
)}
{hasIndexSuggestion && (
<Tooltip>
<TooltipTrigger asChild>
<button
className="flex items-center"
onClick={() => openSheet(column.name as string)}
>
<Lightbulb size={14} strokeWidth={2} className="!text-warning" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" className="font-normal">
Index might improve performance. Click for details.
</TooltipContent>
</Tooltip>
)}
</div>
<ColumnMenu column={column} isEncrypted={isEncrypted} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useQueryClient } from '@tanstack/react-query'
import { RefreshCw } from 'lucide-react'

import { useParams } from 'common'
import { useTableIndexAdvisor } from 'components/grid/context/TableIndexAdvisorContext'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { tableRowKeys } from 'data/table-rows/keys'

Expand All @@ -13,10 +14,12 @@ export type RefreshButtonProps = {
export const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => {
const { ref } = useParams()
const queryClient = useQueryClient()
const { invalidate: invalidateIndexAdvisor } = useTableIndexAdvisor()
const queryKey = tableRowKeys.tableRowsAndCount(ref, tableId)

async function onClick() {
await queryClient.invalidateQueries({ queryKey })
await invalidateIndexAdvisor()
}

return (
Expand Down
144 changes: 144 additions & 0 deletions apps/studio/components/grid/context/TableIndexAdvisorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { useQueryClient } from '@tanstack/react-query'
import { createContext, useContext, PropsWithChildren, useState, useCallback } from 'react'

import { QueryIndexes } from 'components/interfaces/QueryPerformance/QueryIndexes'
import { useIndexAdvisorStatus } from 'components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
import { databaseKeys } from 'data/database/keys'
import {
useTableIndexAdvisorQuery,
TableIndexAdvisorData,
IndexAdvisorSuggestion,
} from 'data/database/table-index-advisor-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Sheet, SheetContent, SheetHeader, SheetTitle } from 'ui'

interface TableIndexAdvisorContextValue {
isLoading: boolean
isAvailable: boolean
isEnabled: boolean
columnsWithSuggestions: string[]
suggestions: TableIndexAdvisorData['suggestions']
openSheet: (columnName: string) => void
getSuggestionsForColumn: (columnName: string) => IndexAdvisorSuggestion[]
invalidate: () => Promise<void>
}

const TableIndexAdvisorContext = createContext<TableIndexAdvisorContextValue>({
isLoading: false,
isAvailable: false,
isEnabled: false,
columnsWithSuggestions: [],
suggestions: [],
openSheet: () => {},
getSuggestionsForColumn: () => [],
invalidate: async () => {},
})

interface TableIndexAdvisorProviderProps {
schema: string
table: string
}

export function TableIndexAdvisorProvider({
children,
schema,
table,
}: PropsWithChildren<TableIndexAdvisorProviderProps>) {
const { data: project } = useSelectedProjectQuery()
const { isIndexAdvisorAvailable, isIndexAdvisorEnabled } = useIndexAdvisorStatus()
const queryClient = useQueryClient()
const [isSheetOpen, setIsSheetOpen] = useState(false)
const [selectedColumn, setSelectedColumn] = useState<string | undefined>(undefined)

const { data, isLoading } = useTableIndexAdvisorQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema,
table,
},
{
enabled: isIndexAdvisorEnabled && !!schema && !!table,
}
)

const openSheet = useCallback((columnName: string) => {
setSelectedColumn(columnName)
setIsSheetOpen(true)
}, [])

const closeSheet = useCallback(() => {
setIsSheetOpen(false)
setSelectedColumn(undefined)
}, [])

const getSuggestionsForColumn = useCallback(
(columnName: string): IndexAdvisorSuggestion[] => {
if (!data?.suggestions) return []
// Filter suggestions that include this column in their index statements
return data.suggestions.filter((suggestion) =>
suggestion.index_statements.some((stmt) => {
const match = stmt.match(/USING\s+\w+\s*\(([^)]+)\)/i)
if (match) {
const columns = match[1].split(',').map((c) => c.trim().replace(/^"(.+)"$/, '$1'))
return columns.includes(columnName)
}
return false
})
)
},
[data?.suggestions]
)

const invalidate = useCallback(async () => {
if (project?.ref && schema && table) {
await queryClient.invalidateQueries({
queryKey: databaseKeys.tableIndexAdvisor(project.ref, schema, table),
})
}
}, [queryClient, project?.ref, schema, table])

// Get the first suggestion for the selected column to pass to QueryIndexes
const selectedSuggestion = selectedColumn ? getSuggestionsForColumn(selectedColumn)[0] : null

const value: TableIndexAdvisorContextValue = {
isLoading,
isAvailable: isIndexAdvisorAvailable,
isEnabled: isIndexAdvisorEnabled,
columnsWithSuggestions: data?.columnsWithSuggestions ?? [],
suggestions: data?.suggestions ?? [],
openSheet,
getSuggestionsForColumn,
invalidate,
}

return (
<TableIndexAdvisorContext.Provider value={value}>
{children}
<Sheet open={isSheetOpen} onOpenChange={(open) => !open && closeSheet()}>
<SheetContent className="flex flex-col gap-0 p-0 sm:max-w-[500px]">
<SheetHeader className="border-b px-5 py-3">
<SheetTitle>Index Recommendation</SheetTitle>
</SheetHeader>
{selectedSuggestion && (
<QueryIndexes
selectedRow={{ query: selectedSuggestion.query }}
columnName={selectedColumn}
suggestedSelectQuery={selectedSuggestion.query}
onClose={closeSheet}
/>
)}
</SheetContent>
</Sheet>
</TableIndexAdvisorContext.Provider>
)
}

export function useTableIndexAdvisor() {
return useContext(TableIndexAdvisorContext)
}

export function useColumnHasIndexSuggestion(columnName: string): boolean {
const { columnsWithSuggestions } = useTableIndexAdvisor()
return columnsWithSuggestions.includes(columnName)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Check, Lightbulb, Table2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Check, Table2, Lightbulb } from 'lucide-react'
import { useState, useEffect } from 'react'

import { AccordionTrigger } from '@ui/components/shadcn/ui/accordion'
import { useIndexAdvisorStatus } from 'components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
Expand Down Expand Up @@ -33,18 +33,28 @@ import {
createIndexes,
hasIndexRecommendations,
} from './IndexAdvisor/index-advisor.utils'
import { QueryPerformanceRow } from './QueryPerformance.types'
import { IndexAdvisorDisabledState } from './IndexAdvisor/IndexAdvisorDisabledState'
import { IndexImprovementText } from './IndexAdvisor/IndexImprovementText'
import { QueryPanelContainer, QueryPanelScoreSection, QueryPanelSection } from './QueryPanel'

interface QueryIndexesProps {
selectedRow: any
selectedRow: Pick<QueryPerformanceRow, 'query'>
columnName?: string
suggestedSelectQuery?: string

onClose?: () => void
}

// [Joshen] There's several more UX things we can do to help ease the learning curve of indexes I think
// e.g understanding "costs", what numbers of "costs" are actually considered insignificant

export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
export const QueryIndexes = ({
selectedRow,
columnName,
suggestedSelectQuery,
onClose,
}: QueryIndexesProps) => {
// [Joshen] TODO implement this logic once the linter rules are in
const isLinterWarning = false
const { data: project } = useSelectedProjectQuery()
Expand Down Expand Up @@ -142,6 +152,8 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
setIsExecuting(false)
} finally {
setIsExecuting(false)

onClose?.()
}
}

Expand All @@ -165,7 +177,36 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
}

return (
<QueryPanelContainer className="h-full">
<QueryPanelContainer className="h-full overflow-y-auto py-0 pt-4">
{(columnName || suggestedSelectQuery) && (
<QueryPanelSection className="pt-2 pb-6 border-b">
<div className="flex flex-col gap-y-3">
<div>
<h4 className="mb-2">Recommendation reason</h4>
{columnName && (
<p className="text-sm text-foreground-light">
Recommendation for column: <span className="font-mono">{columnName}</span>
</p>
)}
</div>
{suggestedSelectQuery && (
<div className="flex flex-col gap-y-4">
<p className="text-sm text-foreground-light">Based on the following query:</p>
<CodeBlock
hideLineNumbers
value={suggestedSelectQuery}
language="sql"
className={cn(
'max-w-full max-h-[200px]',
'!py-2 !px-2.5 prose dark:prose-dark',
'[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
)}
/>
</div>
)}
</div>
</QueryPanelSection>
)}
<QueryPanelSection className="pt-2 mb-6">
<div className="mb-4 flex flex-col gap-y-1">
<h4 className="mb-2">Indexes in use</h4>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
QUERY_PERFORMANCE_REPORT_TYPES,
} from '../QueryPerformance.constants'
import { useIndexAdvisorStatus } from './useIsIndexAdvisorStatus'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import { useTableIndexAdvisor } from 'components/grid/context/TableIndexAdvisorContext'

export function useIndexInvalidation() {
const router = useRouter()
Expand All @@ -29,6 +31,8 @@ export function useIndexInvalidation() {
preset: parseAsString.withDefault('unified'),
})

const { invalidate: invalidateTableIndexAdvisor } = useTableIndexAdvisor()

const preset = QUERY_PERFORMANCE_PRESET_MAP[urlPreset as QUERY_PERFORMANCE_REPORT_TYPES]
const orderBy = !!sort ? ({ column: sort, order } as QueryPerformanceSort) : undefined
const roles = router?.query?.roles ?? []
Expand All @@ -47,5 +51,7 @@ export function useIndexInvalidation() {
queryKey: databaseKeys.indexAdvisorFromQuery(project?.ref, ''),
})
queryClient.invalidateQueries({ queryKey: databaseIndexesKeys.list(project?.ref) })
}, [queryPerformanceQuery, queryClient, project?.ref])

invalidateTableIndexAdvisor()
}, [queryPerformanceQuery, queryClient, project?.ref, invalidateTableIndexAdvisor])
}
20 changes: 17 additions & 3 deletions apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useEffect, useState } from 'react'
import { toast } from 'sonner'

import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useCheckOpenAIKeyQuery } from 'data/ai/check-api-key-query'
import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { getContentById } from 'data/content/content-id-query'
Expand All @@ -13,6 +15,7 @@ import { Snippet } from 'data/content/sql-folders-query'
import type { SqlSnippet } from 'data/content/sql-snippets-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useRouter } from 'next/router'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
import { AiIconAnimation, Button, Form, Input, Modal } from 'ui'
Expand All @@ -32,6 +35,7 @@ const RenameQueryModal = ({
onComplete,
}: RenameQueryModalProps) => {
const { ref } = useParams()
const router = useRouter()
const { data: organization } = useSelectedOrganizationQuery()

const snapV2 = useSqlEditorV2StateSnapshot()
Expand Down Expand Up @@ -61,6 +65,8 @@ const RenameQueryModal = ({
toast.error(`Failed to rename query: ${error.message}`)
},
})
const { data: check } = useCheckOpenAIKeyQuery()
const isApiKeySet = !!check?.hasKey

const generateTitle = async () => {
if ('content' in snippet && isSQLSnippet) {
Expand Down Expand Up @@ -149,19 +155,27 @@ const RenameQueryModal = ({
/>
<div className="flex w-full justify-end mt-2">
{!hasHipaaAddon && (
<Button
<ButtonTooltip
type="default"
onClick={() => generateTitle()}
size="tiny"
disabled={isTitleGenerationLoading}
disabled={isTitleGenerationLoading || !isApiKeySet}
tooltip={{
content: {
side: 'bottom',
text: isApiKeySet
? undefined
: 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.',
},
}}
>
<div className="flex items-center gap-1">
<div className="scale-75">
<AiIconAnimation loading={isTitleGenerationLoading} />
</div>
<span>Rename with Supabase AI</span>
</div>
</Button>
</ButtonTooltip>
)}
</div>
<Input.TextArea
Expand Down
Loading
Loading