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
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const DeleteDestination = ({
visible={visible}
loading={isLoading}
title="Delete this destination"
confirmLabel={isLoading ? 'Deleting' : `Delete destination`}
confirmLabel={isLoading ? 'Deleting...' : `Delete destination`}
confirmPlaceholder="Type in name of destination"
confirmString={name ?? 'Unknown'}
text={`This will delete the destination "${name}"`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const SnippetDropdown = ({
/>
<CommandList_Shadcn_ ref={scrollRootRef}>
{isLoading ? (
<CommandEmpty_Shadcn_>Loading</CommandEmpty_Shadcn_>
<CommandEmpty_Shadcn_>Loading...</CommandEmpty_Shadcn_>
) : snippets.length === 0 ? (
<CommandEmpty_Shadcn_>No snippets found</CommandEmpty_Shadcn_>
) : null}
Expand Down
166 changes: 97 additions & 69 deletions apps/studio/components/interfaces/Reports/ReportBlock/ReportBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { X } from 'lucide-react'
import { useCallback, useState } from 'react'
import { toast } from 'sonner'

import { useParams } from 'common'
import { ChartConfig } from 'components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DEFAULT_CHART_CONFIG, QueryBlock } from 'components/ui/QueryBlock/QueryBlock'
import { AnalyticsInterval } from 'data/analytics/constants'
import { useContentIdQuery } from 'data/content/content-id-query'
import { usePrimaryDatabase } from 'data/read-replicas/replicas-query'
import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { useChangedSync } from 'hooks/misc/useChanged'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { Dashboards, SqlSnippets } from 'types'
import { Button, cn } from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import type { Dashboards, SqlSnippets } from 'types'
import { DEPRECATED_REPORTS } from '../Reports.constants'
import { ChartBlock } from './ChartBlock'
import { DeprecatedChartBlock } from './DeprecatedChartBlock'
Expand Down Expand Up @@ -46,7 +49,7 @@ export const ReportBlock = ({

const isSnippet = item.attribute.startsWith('snippet_')

const { data, error, isLoading, isError } = useContentIdQuery(
const { data, error, isLoading } = useContentIdQuery(
{ projectRef, id: item.id },
{
enabled: isSnippet && !!item.id,
Expand All @@ -57,89 +60,114 @@ export const ReportBlock = ({
if (failureCount >= 2) return false
return true
},
onSuccess: (contentData) => {
if (!isSnippet) return
const fetchedSql = (contentData?.content as SqlSnippets.Content | undefined)?.sql
if (fetchedSql) runQuery('select', fetchedSql)
},
}
)
const sql = isSnippet ? (data?.content as SqlSnippets.Content)?.sql : undefined
const chartConfig = { ...DEFAULT_CHART_CONFIG, ...(item.chartConfig ?? {}) }
const isDeprecatedChart = DEPRECATED_REPORTS.includes(item.attribute)
const snippetMissing = error?.message.includes('Content not found')

const { database: primaryDatabase } = usePrimaryDatabase({ projectRef })
const readOnlyConnectionString = primaryDatabase?.connection_string_read_only
const postgresConnectionString = primaryDatabase?.connectionString

const [rows, setRows] = useState<any[] | undefined>(undefined)
const [isWriteQuery, setIsWriteQuery] = useState(false)

const {
mutate: executeSql,
error: executeSqlError,
isLoading: executeSqlLoading,
} = useExecuteSqlMutation({
onError: () => {
// Silence the error toast because the error will be displayed inline
},
})

const runQuery = useCallback(
(queryType: 'select' | 'mutation' = 'select', sqlToRun?: string) => {
if (!projectRef || !sqlToRun) return false

const connectionString =
queryType === 'mutation'
? postgresConnectionString
: readOnlyConnectionString ?? postgresConnectionString

if (!connectionString) {
toast.error('Unable to establish a database connection for this project.')
return false
}

if (queryType === 'mutation') {
setIsWriteQuery(true)
}
executeSql(
{ projectRef, connectionString, sql: sqlToRun },
{
onSuccess: (data) => {
setRows(data.result)
setIsWriteQuery(queryType === 'mutation')
},
onError: (mutationError) => {
const lowerMessage = mutationError.message.toLowerCase()
const isReadOnlyError =
lowerMessage.includes('read-only transaction') ||
lowerMessage.includes('permission denied') ||
lowerMessage.includes('must be owner')

if (queryType === 'select' && isReadOnlyError) {
setIsWriteQuery(true)
}
},
}
)
return true
},
[projectRef, readOnlyConnectionString, postgresConnectionString, executeSql]
)

const sqlHasChanged = useChangedSync(sql)
const isRefreshingChanged = useChangedSync(isRefreshing)
if (sqlHasChanged || (isRefreshingChanged && isRefreshing)) {
runQuery('select', sql)
}

return (
<>
{isSnippet ? (
<QueryBlock
runQuery
isChart
draggable
blockWriteQueries
id={item.id}
isLoading={isLoading}
isRefreshing={isRefreshing}
label={item.label}
chartConfig={chartConfig}
sql={sql}
maxHeight={232}
queryHeight={232}
results={rows}
initialHideSql={true}
errorText={snippetMissing ? 'SQL snippet not found' : executeSqlError?.message}
isExecuting={executeSqlLoading}
isWriteQuery={isWriteQuery}
actions={
<ButtonTooltip
type="text"
icon={<X />}
className="w-7 h-7"
onClick={() => onRemoveChart({ metric: { key: item.attribute } })}
tooltip={{ content: { side: 'bottom', text: 'Remove chart' } }}
/>
}
onUpdateChartConfig={onUpdateChart}
noResultPlaceholder={
<div
className={cn(
'flex flex-col gap-y-1 h-full w-full',
isLoading ? 'justify-start items-start p-2 gap-y-2' : 'justify-center px-4 gap-y-1'
)}
>
{isLoading ? (
<>
<ShimmeringLoader className="w-full" />
<ShimmeringLoader className="w-full w-3/4" />
<ShimmeringLoader className="w-full w-1/2" />
</>
) : isError ? (
<>
<p className="text-xs text-foreground-light text-center">
{snippetMissing ? 'SQL snippet cannot be found' : 'Error fetching SQL snippet'}
</p>
<p className="text-xs text-foreground-lighter text-center">
{snippetMissing ? 'Please remove this block from your report' : error.message}
</p>
</>
) : (
<>
<p className="text-xs text-foreground-light text-center">
No results returned from query
</p>
<p className="text-xs text-foreground-lighter text-center">
Results from the SQL query can be viewed as a table or chart here
</p>
</>
)}
</div>
}
readOnlyErrorPlaceholder={
<div className="flex flex-col h-full justify-center items-center text-center">
<p className="text-xs text-foreground-light">
SQL query is not read-only and cannot be rendered
</p>
<p className="text-xs text-foreground-lighter text-center">
Queries that involve any mutation will not be run in reports
</p>
<Button
type="default"
className="mt-2"
!isLoading && (
<ButtonTooltip
type="text"
icon={<X />}
className="w-7 h-7"
onClick={() => onRemoveChart({ metric: { key: item.attribute } })}
>
Remove chart
</Button>
</div>
tooltip={{ content: { side: 'bottom', text: 'Remove chart' } }}
/>
)
}
onExecute={(queryType) => {
runQuery(queryType, sql)
}}
onUpdateChartConfig={onUpdateChart}
onRemoveChart={() => onRemoveChart({ metric: { key: item.attribute } })}
disabled={isLoading || snippetMissing || !sql}
/>
) : isDeprecatedChart ? (
<DeprecatedChartBlock
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { GripHorizontal, Loader2 } from 'lucide-react'
import { Code, GripHorizontal } from 'lucide-react'
import { DragEvent, PropsWithChildren, ReactNode } from 'react'

import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'

interface ReportBlockContainerProps {
icon: ReactNode
icon?: ReactNode
label: string
badge?: ReactNode
actions: ReactNode
loading?: boolean
draggable?: boolean
Expand All @@ -16,6 +18,7 @@ interface ReportBlockContainerProps {
export const ReportBlockContainer = ({
icon,
label,
badge,
actions,
loading = false,
draggable = false,
Expand All @@ -39,35 +42,28 @@ export const ReportBlockContainer = ({
<TooltipTrigger asChild>
<div
className={cn(
'grid-item-drag-handle flex py-1 pl-3 pr-1 items-center gap-2 z-10 shrink-0 group',
'grid-item-drag-handle flex py-1 pl-3 pr-1 items-center gap-2 z-10 shrink-0 group h-9',
draggable && 'cursor-move'
)}
>
<div
className={cn(
showDragHandle && 'transition-opacity opacity-100 group-hover:opacity-0'
)}
>
{loading ? (
<Loader2
size={(icon as any)?.props?.size ?? 16}
className="text-foreground-lighter animate-spin"
/>
) : (
icon
)}
</div>
{showDragHandle && (
{showDragHandle ? (
<div className="absolute left-3 top-2.5 z-10 opacity-0 transition-opacity group-hover:opacity-100">
<GripHorizontal size={16} strokeWidth={1.5} />
</div>
) : icon ? (
icon
) : (
<Code size={16} strokeWidth={1.5} className="text-foreground-muted" />
)}
<h3
title={label}
className="!text-xs font-medium text-foreground-light flex-1 truncate"
<div
className={cn(
'flex items-center gap-2 flex-1 transition-opacity',
showDragHandle && 'group-hover:opacity-25'
)}
>
{label}
</h3>
<h3 className="heading-meta truncate">{label}</h3>
{badge && <div className="flex items-center shrink-0">{badge}</div>}
</div>
<div className="flex items-center">{actions}</div>
</div>
</TooltipTrigger>
Expand All @@ -77,8 +73,20 @@ export const ReportBlockContainer = ({
</TooltipContent>
)}
</Tooltip>
<div className={cn('flex flex-col flex-grow items-center', hasChildren && 'border-t')}>
{children}
<div
className={cn(
'relative flex flex-col flex-grow w-full',
hasChildren && 'border-t overflow-hidden'
)}
>
<div
className={cn(
'flex flex-col flex-grow items-center overflow-hidden',
loading && 'pointer-events-none'
)}
>
{children}
</div>
</div>
</div>
)
Expand Down
Loading
Loading