// @ts-nocheck import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { groupBy, isEqual, isNull } from 'lodash' import { Plus, RefreshCw, Save } from 'lucide-react' import { DragEvent, useEffect, useState } from 'react' import { toast } from 'sonner' import { Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, LogoLoader } from 'ui' import { createSqlSnippetSkeletonV2 } from '../SQLEditor/SQLEditor.utils' import { ChartConfig } from '../SQLEditor/UtilityPanel/ChartConfig' import { GridResize } from './GridResize' import { MetricOptions } from './MetricOptions' import { LAYOUT_COLUMN_COUNT } from './Reports.constants' import { PreventNavigationOnUnsavedChanges } from '@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { DatabaseSelector } from '@/components/ui/DatabaseSelector' import { DateRangePicker } from '@/components/ui/DateRangePicker' import NoPermission from '@/components/ui/NoPermission' 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 { UpsertContentPayload, useContentUpsertMutation, } from '@/data/content/content-upsert-mutation' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { Metric, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics' import { uuidv4 } from '@/lib/helpers' import { useProfile } from '@/lib/profile' import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' import type { Dashboards } from '@/types' const DEFAULT_CHART_COLUMN_COUNT = 1 const DEFAULT_CHART_ROW_COUNT = 1 const Reports = () => { const { id: reportId, ref } = useParams() const { profile } = useProfile() const { data: project } = useSelectedProjectQuery() const { data: selectedOrg } = useSelectedOrganizationQuery() const queryClient = useQueryClient() const state = useDatabaseSelectorStateSnapshot() const [isDraggedOver, setIsDraggedOver] = useState(false) const [config, setConfig] = useState() const [startDate, setStartDate] = useState() const [endDate, setEndDate] = useState() const [hasEdits, setHasEdits] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) const { data: userContents, isPending: isLoading, isSuccess, } = useContentQuery({ projectRef: ref, type: 'report', }) const { mutate: upsertContent, isPending: isSaving } = useContentUpsertMutation({ onSuccess: (_, vars) => { setHasEdits(false) if (vars.payload.type === 'report') toast.success('Successfully saved report!') }, 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 === reportId) const currentReportContent = currentReport?.content as Dashboards.Content const { can: canReadReport, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( PermissionAction.READ, 'user_content', { resource: { type: 'report', visibility: currentReport?.visibility, owner_id: currentReport?.owner_id, }, subject: { id: profile?.id }, } ) const { can: canUpdateReport } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'user_content', { resource: { type: 'report', visibility: currentReport?.visibility, owner_id: currentReport?.owner_id, }, subject: { id: profile?.id }, } ) function handleDateRangePicker({ period_start, period_end }: any) { setStartDate(period_start.date) setEndDate(period_end.date) } function checkEditState() { if (config === undefined) return /* * Shallow copying the config state variable maintains a reference * Instead, we stringify it and parse it again to remove anything * that can be mutated at component state level. * * This allows us to mutate these configs, like removing dates in case we do not * want to compare fixed dates as possible differences from saved and edited versions of report. */ let _config = JSON.parse(JSON.stringify(config)) let _original = JSON.parse(JSON.stringify(currentReportContent)) if (!_original || !_config) return /* * Check if the dates are a fixed custom date range * if they are not, we remove the dates for the edit check comparison * * this feature is not yet in use, but if we did use custom fixed date ranges, * the below would not need to be run */ if ( _config.period_start.time_period !== 'custom' || _config.period_end.time_period !== 'custom' ) { _original.period_start.date = '' _config.period_start.date = '' _original.period_end.date = '' _config.period_end.date = '' } // Runs comparison if (isEqual(_config, _original)) { setHasEdits(false) } else { setHasEdits(true) } } const handleChartSelection = ({ metric, isAddingChart, }: { metric: Metric isAddingChart: boolean }) => { if (isAddingChart) pushChart({ metric }) else popChart({ metric }) } const pushChart = ({ metric }: { metric: Metric }) => { if (!config) return const current = [...config.layout] let x = 0 let y = null const chartsByY = groupBy(config.layout, 'y') const yValues = Object.keys(chartsByY) const isSnippet = metric.key?.startsWith('snippet_') if (yValues.length === 0) { y = 0 } else { // Find if any row has space to fit in a new chart for (const yValue of yValues) { const totalWidthTaken = chartsByY[yValue].reduce((a, b) => a + b.w, 0) if (LAYOUT_COLUMN_COUNT - totalWidthTaken >= DEFAULT_CHART_COLUMN_COUNT) { y = Number(yValue) // Given that there can not be any gaps between charts, it's safe to // assume that we can set x using the accumulative widths x = totalWidthTaken break } } // If no rows have space to fit the new chart, bring it to a new row if (isNull(y)) { y = Number(yValues[yValues.length - 1]) + DEFAULT_CHART_ROW_COUNT } } current.push({ x, y, w: DEFAULT_CHART_COLUMN_COUNT, h: DEFAULT_CHART_ROW_COUNT, id: metric?.id ?? uuidv4(), label: metric.label, attribute: metric.key as Dashboards.ChartType, provider: metric.provider as any, chart_type: 'bar', ...(isSnippet ? { chartConfig: DEFAULT_CHART_CONFIG } : {}), }) setConfig({ ...config, layout: [...current], }) } const popChart = ({ metric }: { metric: Partial }) => { if (!config) return const { key, id } = metric const current = [...config.layout] const foundIndex = current.findIndex((x) => { if (x.attribute === key || x.id === id) return x }) current.splice(foundIndex, 1) setConfig({ ...config, layout: [...current] }) } const updateChart = ( id: string, { chart, chartConfig, }: { chart?: Partial; chartConfig?: Partial } ) => { const currentChart = config?.layout.find((x) => x.id === id) if (currentChart) { const updatedChart: Dashboards.Chart = { ...currentChart, ...(chart ?? {}), } if (chartConfig) { updatedChart.chartConfig = { ...(currentChart?.chartConfig ?? {}), ...chartConfig } } const foundIndex = config?.layout.findIndex((x) => x.id === id) if (config && foundIndex !== undefined && foundIndex >= 0) { const updatedLayouts = [...config.layout] updatedLayouts[foundIndex] = updatedChart setConfig({ ...config, layout: updatedLayouts }) } } } // Updates the report and reloads the report again const onSaveReport = async () => { if (ref === undefined) return console.error('Project ref is required') if (currentReport === undefined) return console.error('Report is required') if (config === undefined) return console.error('Config is required') upsertContent({ projectRef: ref, payload: { ...currentReport, content: config }, }) } const onRefreshReport = () => { // [Joshen] Since we can't track individual loading states for each chart // so for now we mock a loading state that only lasts for a second setIsRefreshing(true) const monitoringCharts = config?.layout.filter( (x) => x.provider === 'infra-monitoring' || x.provider === 'daily-stats' ) monitoringCharts?.forEach((x) => { queryClient.invalidateQueries({ queryKey: analyticsKeys.infraMonitoring(ref, { attribute: x.attribute, startDate, endDate, interval: config?.interval, databaseIdentifier: state.selectedDatabaseId, }), }) }) 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 payload = createSqlSnippetSkeletonV2({ name: label, sql, owner_id: profile?.id, project_id: project?.id, }) as UpsertContentPayload const updatedLayout = [...config.layout] updatedLayout.push({ id: payload.id, label, x: 0, y: 0, chart_type: 'bar', attribute: `new_snippet_${payload.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] }) upsertContent( { projectRef: ref, payload }, { onSuccess: () => { toast.success(`Successfully created new query: ${label}`, { id: toastId }) const finalLayout = updatedLayout.map((x) => { if (x.id === payload.id) { return { ...x, attribute: `snippet_${payload.id}` as Dashboards.ChartType } } else return x }) setConfig({ ...config, layout: finalLayout }) }, } ) sendEvent({ action: 'custom_report_assistant_sql_block_added', groups: { project: ref ?? 'Unknown', organization: selectedOrg?.slug ?? 'Unknown' }, }) } useEffect(() => { if (isSuccess && currentReportContent !== undefined) setConfig(currentReportContent) }, [isSuccess, currentReportContent]) useEffect(() => { checkEditState() }, [config]) if (isLoading || isLoadingPermissions) { return } if (!canReadReport) { return } return ( <>

{currentReport?.name || 'Reports'}

{currentReport?.description}

{hasEdits && (
)}
} className="w-7" disabled={isRefreshing} tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }} onClick={onRefreshReport} />

SQL blocks are independent of the selected date range

} />
{canUpdateReport ? ( ) : ( } tooltip={{ content: { side: 'bottom', className: 'w-56 text-center', text: 'You need additional permissions to update custom reports', }, }} > Add block )}
{config?.layout !== undefined && config.layout.length === 0 ? (
{canUpdateReport ? ( ) : (

No charts set up yet in report

)}
) : (
{config && startDate && endDate && ( )}
)} ) } export default Reports