diff --git a/apps/studio/components/interfaces/Reports/Reports.tsx b/apps/studio/components/interfaces/Reports/Reports.tsx index 3b5c517410..d0c239a991 100644 --- a/apps/studio/components/interfaces/Reports/Reports.tsx +++ b/apps/studio/components/interfaces/Reports/Reports.tsx @@ -1,12 +1,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { groupBy, isEqual, isNull } from 'lodash' import { ArrowRight, Plus, RefreshCw, Save } from 'lucide-react' -import { useEffect, useState } from 'react' +import { DragEvent, useEffect, useState } from 'react' import { toast } from 'sonner' -import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import DatabaseSelector from 'components/ui/DatabaseSelector' import { DateRangePicker } from 'components/ui/DateRangePicker' @@ -16,7 +17,11 @@ import { DEFAULT_CHART_CONFIG } from 'components/ui/QueryBlock/QueryBlock' import { AnalyticsInterval } from 'data/analytics/constants' import { analyticsKeys } from 'data/analytics/keys' import { useContentQuery } from 'data/content/content-query' -import { useContentUpsertMutation } from 'data/content/content-upsert-mutation' +import { + UpsertContentPayload, + useContentUpsertMutation, +} from 'data/content/content-upsert-mutation' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Metric, TIME_PERIODS_REPORTS } from 'lib/constants/metrics' import { uuidv4 } from 'lib/helpers' @@ -24,6 +29,7 @@ import { useProfile } from 'lib/profile' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { Dashboards } from 'types' import { Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui' +import { createSqlSnippetSkeletonV2 } from '../SQLEditor/SQLEditor.utils' import { ChartConfig } from '../SQLEditor/UtilityPanel/ChartConfig' import { GridResize } from './GridResize' import { MetricOptions } from './MetricOptions' @@ -35,9 +41,11 @@ const DEFAULT_CHART_ROW_COUNT = 1 const Reports = () => { const { id, ref } = useParams() const { profile } = useProfile() + const { project } = useProjectContext() const queryClient = useQueryClient() const state = useDatabaseSelectorStateSnapshot() + const [isDraggedOver, setIsDraggedOver] = useState(false) const [config, setConfig] = useState() const [startDate, setStartDate] = useState() const [endDate, setEndDate] = useState() @@ -53,14 +61,15 @@ const Reports = () => { type: 'report', }) const { mutate: upsertContent, isLoading: isSaving } = useContentUpsertMutation({ - onSuccess: () => { + onSuccess: (_, vars) => { setHasEdits(false) - toast.success('Successfully saved report!') + if (vars.payload.type === 'report') toast.success('Successfully saved report!') }, - onError: (error) => { - toast.error(`Failed to update report: ${error.message}`) + onError: (error, vars) => { + if (vars.payload.type === 'report') toast.error(`Failed to update report: ${error.message}`) }, }) + const { mutate: sendEvent } = useSendEventMutation() const currentReport = userContents?.content.find((report) => report.id === id) const currentReportContent = currentReport?.content as Dashboards.Content @@ -262,6 +271,74 @@ const Reports = () => { setTimeout(() => setIsRefreshing(false), 1000) } + const onDragOverEmptyState = (event: DragEvent) => { + if (event.type === 'dragover' && !isDraggedOver) { + setIsDraggedOver(true) + } else if (event.type === 'dragleave' || event.type === 'drop') { + setIsDraggedOver(false) + } + event.stopPropagation() + event.preventDefault() + } + + const onDropSQLBlockEmptyState = (event: DragEvent) => { + onDragOverEmptyState(event) + if (!ref) return console.error('Project ref is required') + if (!profile) return console.error('Profile is required') + if (!project) return console.error('Project is required') + if (!config) return console.error('Chart configuration is required') + + const data = event.dataTransfer.getData('application/json') + if (!data) return + + const queryData = JSON.parse(data) + const { label, sql, config: sqlConfig } = queryData + if (!label || !sql) return console.error('SQL and Label required') + + const toastId = toast.loading(`Creating new query: ${label}`) + const id = uuidv4() + + const updatedLayout = [...config.layout] + updatedLayout.push({ + id, + label, + x: 0, + y: 0, + chart_type: 'bar', + attribute: `new_snippet_${id}` as Dashboards.ChartType, + w: DEFAULT_CHART_COLUMN_COUNT, + h: DEFAULT_CHART_ROW_COUNT, + chartConfig: { ...DEFAULT_CHART_CONFIG, ...(sqlConfig ?? {}) }, + provider: undefined as any, + }) + + setConfig({ ...config, layout: [...updatedLayout] }) + + const payload = createSqlSnippetSkeletonV2({ + id, + name: label, + sql, + owner_id: profile?.id, + project_id: project?.id, + }) as UpsertContentPayload + + upsertContent( + { projectRef: ref, payload }, + { + onSuccess: () => { + toast.success(`Successfully created new query: ${label}`, { id: toastId }) + const finalLayout = updatedLayout.map((x) => { + if (x.id === id) { + return { ...x, attribute: `snippet_${id}` as Dashboards.ChartType } + } else return x + }) + setConfig({ ...config, layout: finalLayout }) + }, + } + ) + sendEvent({ action: 'custom_report_assistant_sql_block_added' }) + } + useEffect(() => { if (isSuccess && currentReportContent !== undefined) setConfig(currentReportContent) }, [isSuccess, currentReportContent]) @@ -379,15 +456,21 @@ const Reports = () => { - {config?.layout !== undefined && config.layout.length <= 0 ? ( -
+ {config?.layout !== undefined && config.layout.length === 0 ? ( +
{canUpdateReport ? ( @@ -399,7 +482,7 @@ const Reports = () => { )}
) : ( -
+
{config && startDate && endDate && ( {(queryResult ?? []).length === 0 ? ( -
+

No results returned from query

) : !xKey || !yKey ? (