Reports() — supabase Function Reference
Architecture documentation for the Reports() function in Reports.tsx from the supabase codebase.
Entity Profile
Dependency Diagram
graph TD 4747908b_5a95_788c_9259_edb031563b9d["Reports()"] 2bdac7e5_a25b_6415_051d_1503de098a7f["createSqlSnippetSkeletonV2()"] 4747908b_5a95_788c_9259_edb031563b9d -->|calls| 2bdac7e5_a25b_6415_051d_1503de098a7f style 4747908b_5a95_788c_9259_edb031563b9d fill:#6366f1,stroke:#818cf8,color:#fff
Relationship Graph
Source Code
apps/studio/components/interfaces/Reports/Reports.tsx lines 44–568
const Reports = () => {
const router = useRouter()
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<Dashboards.Content>()
const [startDate, setStartDate] = useState<string>()
const [endDate, setEndDate] = useState<string>()
const [hasEdits, setHasEdits] = useState<boolean>(false)
const [isRefreshing, setIsRefreshing] = useState<boolean>(false)
const [navigateUrl, setNavigateUrl] = useState<string>()
const [confirmNavigate, setConfirmNavigate] = 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<Metric> }) => {
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<Dashboards.Chart>; chartConfig?: Partial<ChartConfig> }
) => {
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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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])
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (hasEdits) {
e.preventDefault()
e.returnValue = '' // deprecated, but older browsers still require this
}
}
const handleBrowseAway = (url: string) => {
if (hasEdits && !confirmNavigate) {
setNavigateUrl(url)
throw 'Route change declined' // Just to prevent the route change
} else {
setNavigateUrl(undefined)
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
router.events.on('routeChangeStart', handleBrowseAway)
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload)
router.events.off('routeChangeStart', handleBrowseAway)
}
}, [hasEdits, confirmNavigate, router])
if (isLoading || isLoadingPermissions) {
return <LogoLoader />
}
if (!canReadReport) {
return <NoPermission isFullPage resourceText="access this custom report" />
}
return (
<>
<div className="flex flex-col space-y-4" style={{ maxHeight: '100%' }}>
<div className="flex items-center justify-between">
<div>
<h1>{currentReport?.name || 'Reports'}</h1>
<p className="text-foreground-light">{currentReport?.description}</p>
</div>
{hasEdits && (
<div className="flex items-center gap-x-2">
<Button
type="default"
disabled={isSaving}
onClick={() => setConfig(currentReportContent)}
>
Cancel
</Button>
<Button
type="primary"
icon={<Save />}
loading={isSaving}
onClick={() => onSaveReport()}
>
Save changes
</Button>
</div>
)}
</div>
<div className={cn('mb-4 flex items-center gap-x-3 justify-between')}>
<div className="flex items-center gap-x-2">
<ButtonTooltip
type="default"
icon={<RefreshCw className={isRefreshing ? 'animate-spin' : ''} />}
className="w-7"
disabled={isRefreshing}
tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }}
onClick={onRefreshReport}
/>
<div className="flex items-center gap-x-3">
<DateRangePicker
value="7d"
className="w-48"
onChange={handleDateRangePicker}
options={TIME_PERIODS_REPORTS}
loading={isLoading}
footer={
<div className="px-2 py-1">
<p className="text-xs text-foreground-lighter">
SQL blocks are independent of the selected date range
</p>
</div>
}
/>
{startDate && endDate && (
<div className="hidden items-center space-x-1 lg:flex ">
<span className="text-sm text-foreground-light">
{dayjs(startDate).format('MMM D, YYYY')}
</span>
<span className="text-foreground-lighter">
<ArrowRight size={12} />
</span>
<span className="text-sm text-foreground-light">
{dayjs(endDate).format('MMM D, YYYY')}
</span>
</div>
)}
</div>
</div>
<div className="flex items-center gap-x-2">
{canUpdateReport ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" icon={<Plus />}>
<span>Add block</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="center" className="w-44">
<MetricOptions config={config} handleChartSelection={handleChartSelection} />
</DropdownMenuContent>
</DropdownMenu>
) : (
<ButtonTooltip
disabled
type="default"
icon={<Plus />}
tooltip={{
content: {
side: 'bottom',
className: 'w-56 text-center',
text: 'You need additional permissions to update custom reports',
},
}}
>
Add block
</ButtonTooltip>
)}
<DatabaseSelector />
</div>
</div>
{config?.layout !== undefined && config.layout.length === 0 ? (
<div
className={cn(
'flex min-h-full items-center justify-center rounded border-2 border-dashed p-16 border-default transition duration-100',
isDraggedOver ? 'bg-surface-100' : ''
)}
onDragOver={onDragOverEmptyState}
onDragLeave={onDragOverEmptyState}
onDrop={onDropSQLBlockEmptyState}
>
{canUpdateReport ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" iconRight={<Plus size={14} />}>
Add your first chart
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="center">
<MetricOptions config={config} handleChartSelection={handleChartSelection} />
</DropdownMenuContent>
</DropdownMenu>
) : (
<p className="text-sm text-foreground-light">No charts set up yet in report</p>
)}
</div>
) : (
<div className="relative mb-16 flex-grow">
{config && startDate && endDate && (
<GridResize
startDate={startDate}
endDate={endDate}
interval={config.interval as AnalyticsInterval}
editableReport={config}
disableUpdate={!canUpdateReport}
isRefreshing={isRefreshing}
onRemoveChart={popChart}
onUpdateChart={updateChart}
setEditableReport={setConfig}
/>
)}
</div>
)}
</div>
<ConfirmationModal
visible={!!navigateUrl}
variant="warning"
title="You have unsaved changes in your report"
confirmLabel="Confirm"
onConfirm={() => {
setConfirmNavigate(true)
let urlToNavigate = navigateUrl ?? '/'
if (BASE_PATH && urlToNavigate.startsWith(BASE_PATH)) {
urlToNavigate = urlToNavigate.slice(BASE_PATH.length) || '/'
}
if (!urlToNavigate.startsWith('/')) urlToNavigate = `/${urlToNavigate}`
setNavigateUrl(undefined)
router.push(urlToNavigate)
}}
onCancel={() => setNavigateUrl(undefined)}
>
<p className="text-sm">
Unsaved changes will be lost, are you sure you want to navigate away?
</p>
</ConfirmationModal>
</>
)
}
Domain
Subdomains
Source
Frequently Asked Questions
What does Reports() do?
Reports() is a function in the supabase codebase.
What does Reports() call?
Reports() calls 1 function(s): createSqlSnippetSkeletonV2.
Analyze Your Own Codebase
Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.
Try Supermodel Free