mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat(studio): rename reports to observability (#40317)
* feat: rename reports to observability on nav sidebar * fix: observability route * feat: observability navigation and redirects * fix: first time load redirects * feat: change auth overview reports link * feat: clean up of other links to observability sections * fix: query perf redirects * chore: find exhaustive deps and disable for time being * fix: charis suggested fixes * fix: forgotten suggested fix * chore: delete old reports views files * fix: paths in reports tests * fix: url sync useEffect should trigger only once * feat: reorganise observability sidebar * fix: data api not being selected * fix: wrong layout for realitime * fix: custom reports reloading --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> Co-authored-by: Ali Waseem <waseema393@gmail.com>
This commit is contained in:
26 files changed
+650
-49
No files matched your search
@@ -12,7 +12,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import Link from 'next/link'
|
||||
import { ChevronRight, ExternalLink, HelpCircle } from 'lucide-react'
|
||||
import { ChevronRight, ExternalLink, HelpCircle, Telescope } from 'lucide-react'
|
||||
import { Reports } from 'icons'
|
||||
import {
|
||||
ScaffoldSection,
|
||||
@@ -231,11 +231,11 @@ export const OverviewMetrics = ({ metrics, isLoading, error }: OverviewMetricsPr
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<ScaffoldSectionTitle>Usage</ScaffoldSectionTitle>
|
||||
<Link
|
||||
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}&isHelper=true&helperText=Last+24+hours`}
|
||||
href={`/project/${ref}/observability/auth?its=${startDate}&ite=${endDate}&isHelper=true&helperText=Last+24+hours`}
|
||||
className="text-sm text-link inline-flex items-center gap-x-1.5"
|
||||
>
|
||||
<Reports size={14} />
|
||||
<span>View all reports</span>
|
||||
<Telescope size={14} />
|
||||
<span>Go to observability</span>
|
||||
<ChevronRight size={14} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ const DatabaseSizeUsage = ({
|
||||
</span>
|
||||
<Button asChild type="default" size={'tiny'}>
|
||||
<Link
|
||||
href={`/project/${project.ref}/reports/database#database-size-report`}
|
||||
href={`/project/${project.ref}/observability/database#database-size-report`}
|
||||
>
|
||||
Manage Database Size
|
||||
</Link>
|
||||
|
||||
@@ -39,7 +39,7 @@ export const CreateReportModal = ({ visible, onCancel, afterSubmit }: CreateRepo
|
||||
onSuccess: (_, vars) => {
|
||||
toast.success('Successfully created new report')
|
||||
const newReportId = vars.payload.id
|
||||
router.push(`/project/${ref}/reports/${newReportId}${preservedQueryParams}`)
|
||||
router.push(`/project/${ref}/observability/${newReportId}${preservedQueryParams}`)
|
||||
afterSubmit()
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
+1
-1
@@ -374,7 +374,7 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
|
||||
className="gap-x-2"
|
||||
disabled={status !== REPLICA_STATUS.ACTIVE_HEALTHY}
|
||||
>
|
||||
<Link href={`/project/${ref}/reports/database?db=${id}&chart=replication-lag`}>
|
||||
<Link href={`/project/${ref}/observability/database?db=${id}&chart=replication-lag`}>
|
||||
View replication lag
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
+1
-1
@@ -315,7 +315,7 @@ const MapView = ({
|
||||
disabled={database.status !== REPLICA_STATUS.ACTIVE_HEALTHY}
|
||||
>
|
||||
<Link
|
||||
href={`/project/${ref}/reports/database?db=${database.identifier}&chart=replication-lag`}
|
||||
href={`/project/${ref}/observability/database?db=${database.identifier}&chart=replication-lag`}
|
||||
>
|
||||
View replication lag
|
||||
</Link>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const generateAdvisorsMenu = (
|
||||
{
|
||||
name: 'Query Performance',
|
||||
key: 'query-performance',
|
||||
url: `/project/${ref}/reports/query-performance`,
|
||||
url: `/project/${ref}/observability/query-performance`,
|
||||
items: [],
|
||||
rightIcon: <ArrowUpRight size={14} strokeWidth={1.5} className="h-4 w-4" />,
|
||||
},
|
||||
|
||||
@@ -176,7 +176,7 @@ export const generateDatabaseMenu = (
|
||||
{
|
||||
name: 'Query Performance',
|
||||
key: 'query-performance',
|
||||
url: `/project/${ref}/reports/query-performance`,
|
||||
url: `/project/${ref}/observability/query-performance`,
|
||||
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
|
||||
items: [],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import type { CommandOptions, ICommand } from 'ui-patterns/CommandMenu'
|
||||
import { orderSectionFirst, useQuery, useRegisterCommands } from 'ui-patterns/CommandMenu'
|
||||
|
||||
const QUERY_PERFORMANCE_COMMAND_ID = 'nav-reports-query-performance'
|
||||
|
||||
export function useReportsGotoCommands(options?: CommandOptions) {
|
||||
let { ref } = useParams()
|
||||
ref ||= '_'
|
||||
|
||||
const { reportsAll } = useIsFeatureEnabled(['reports:all'])
|
||||
|
||||
const commandQuery = useQuery()?.toLowerCase() ?? ''
|
||||
const prioritizeQueryPerformance = commandQuery.includes('query')
|
||||
|
||||
const orderQueryPerformanceCommand = useMemo(() => {
|
||||
if (!prioritizeQueryPerformance) return undefined
|
||||
|
||||
return (existingCommands: ICommand[], incomingCommands: ICommand[]) => {
|
||||
const filteredExisting = existingCommands.filter(
|
||||
(command) => command.id !== QUERY_PERFORMANCE_COMMAND_ID
|
||||
)
|
||||
|
||||
return [...incomingCommands, ...filteredExisting]
|
||||
}
|
||||
}, [prioritizeQueryPerformance])
|
||||
|
||||
const orderNavigateSection = useMemo<CommandOptions['orderSection'] | undefined>(() => {
|
||||
return prioritizeQueryPerformance ? orderSectionFirst : options?.orderSection
|
||||
}, [options?.orderSection, prioritizeQueryPerformance])
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.NAVIGATE,
|
||||
reportsAll
|
||||
? [
|
||||
{
|
||||
id: 'nav-reports',
|
||||
name: 'Reports',
|
||||
route: `/project/${ref}/observability`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-api',
|
||||
name: 'API Reports',
|
||||
route: `/project/${ref}/observability/api-overview`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-storage',
|
||||
name: 'Storage Reports',
|
||||
route: `/project/${ref}/observability/storage`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-database',
|
||||
name: 'Database Reports',
|
||||
route: `/project/${ref}/observability/database`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
{
|
||||
...options,
|
||||
orderSection: orderNavigateSection,
|
||||
deps: [ref, orderNavigateSection, ...(options?.deps ?? [])],
|
||||
}
|
||||
)
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.NAVIGATE,
|
||||
reportsAll
|
||||
? [
|
||||
{
|
||||
id: QUERY_PERFORMANCE_COMMAND_ID,
|
||||
name: 'Query Performance Reports',
|
||||
route: `/project/${ref}/observability/query-performance`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
{
|
||||
...options,
|
||||
orderCommands: orderQueryPerformanceCommand ?? options?.orderCommands,
|
||||
orderSection: orderNavigateSection,
|
||||
deps: [ref, orderQueryPerformanceCommand, orderNavigateSection, ...(options?.deps ?? [])],
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { UnknownInterface } from 'components/ui/UnknownInterface'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { withAuth } from 'hooks/misc/withAuth'
|
||||
import { ProjectLayout } from '../ProjectLayout'
|
||||
import ObservabilityMenu from './ObservabilityMenu'
|
||||
|
||||
interface ObservabilityLayoutProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
const ObservabilityLayout = ({ title, children }: PropsWithChildren<ObservabilityLayoutProps>) => {
|
||||
const { ref } = useParams()
|
||||
const { reportsAll } = useIsFeatureEnabled(['reports:all'])
|
||||
|
||||
if (reportsAll) {
|
||||
return (
|
||||
<ProjectLayout
|
||||
title={title}
|
||||
product="Observability"
|
||||
productMenu={<ObservabilityMenu />}
|
||||
isBlocking={false}
|
||||
>
|
||||
{children}
|
||||
</ProjectLayout>
|
||||
)
|
||||
} else {
|
||||
return <UnknownInterface urlBack={`/project/${ref}`} />
|
||||
}
|
||||
}
|
||||
|
||||
export default withAuth(ObservabilityLayout)
|
||||
@@ -0,0 +1,348 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Plus } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useFlag, useParams } from 'common'
|
||||
import { CreateReportModal } from 'components/interfaces/Reports/CreateReportModal'
|
||||
import { UpdateCustomReportModal } from 'components/interfaces/Reports/UpdateModal'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
|
||||
import { Content, useContentQuery } from 'data/content/content-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { Menu, cn } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { ObservabilityMenuItem } from './ObservabilityMenuItem'
|
||||
import { InnerSideBarEmptyPanel } from 'ui-patterns'
|
||||
|
||||
const ObservabilityMenu = () => {
|
||||
const router = useRouter()
|
||||
const { profile } = useProfile()
|
||||
const { ref, id } = useParams()
|
||||
const pageKey = (id || router.pathname.split('/')[4]) as string
|
||||
const authEnabled = useFlag('authreportv2')
|
||||
const edgeFnEnabled = useFlag('edgefunctionreport')
|
||||
const realtimeEnabled = useFlag('realtimeReport')
|
||||
const storageReportEnabled = useFlag('storagereport')
|
||||
const postgrestReportEnabled = useFlag('postgrestreport')
|
||||
|
||||
// b/c fly doesn't support storage
|
||||
const storageSupported = useIsFeatureEnabled('project_storage:all')
|
||||
const storageEnabled = storageReportEnabled && storageSupported
|
||||
|
||||
const { can: canCreateCustomReport } = useAsyncCheckPermissions(
|
||||
PermissionAction.CREATE,
|
||||
'user_content',
|
||||
{
|
||||
resource: { type: 'report', owner_id: profile?.id },
|
||||
subject: { id: profile?.id },
|
||||
}
|
||||
)
|
||||
|
||||
// Preserve date range query parameters when navigating
|
||||
const preservedQueryParams = useMemo(() => {
|
||||
const { its, ite, isHelper, helperText } = router.query
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (its && typeof its === 'string') params.set('its', its)
|
||||
if (ite && typeof ite === 'string') params.set('ite', ite)
|
||||
if (isHelper && typeof isHelper === 'string') params.set('isHelper', isHelper)
|
||||
if (helperText && typeof helperText === 'string') params.set('helperText', helperText)
|
||||
|
||||
const queryString = params.toString()
|
||||
return queryString ? `?${queryString}` : ''
|
||||
}, [router.query])
|
||||
|
||||
const { data: content, isLoading } = useContentQuery({
|
||||
projectRef: ref,
|
||||
type: 'report',
|
||||
})
|
||||
const { mutate: deleteReport, isLoading: isDeleting } = useContentDeleteMutation({
|
||||
onSuccess: () => {
|
||||
setDeleteModalOpen(false)
|
||||
toast.success('Successfully deleted report')
|
||||
router.push(`/project/${ref}/observability`)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to delete report: ${error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||
const [showNewReportModal, setShowNewReportModal] = useState(false)
|
||||
const [selectedReportToDelete, setSelectedReportToDelete] = useState<Content>()
|
||||
const [selectedReportToUpdate, setSelectedReportToUpdate] = useState<Content>()
|
||||
|
||||
const onConfirmDeleteReport = () => {
|
||||
if (ref === undefined) return console.error('Project ref is required')
|
||||
if (selectedReportToDelete?.id === undefined) return console.error('Report ID is required')
|
||||
deleteReport({ projectRef: ref, ids: [selectedReportToDelete.id] })
|
||||
}
|
||||
|
||||
function getReportMenuItems() {
|
||||
if (!content) return []
|
||||
|
||||
const reports = content?.content.filter((c) => c.type === 'report')
|
||||
|
||||
const sortedReports = reports?.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const reportMenuItems = sortedReports.map((r, idx) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
description: r.description || '',
|
||||
key: r.id || idx + '-report',
|
||||
url: `/project/${ref}/observability/${r.id}${preservedQueryParams}`,
|
||||
hasDropdownActions: true,
|
||||
report: r,
|
||||
}))
|
||||
|
||||
return reportMenuItems
|
||||
}
|
||||
|
||||
const reportMenuItems = getReportMenuItems()
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
title: 'Performance Reports',
|
||||
key: 'performance-reports',
|
||||
items: [
|
||||
{
|
||||
name: 'API Gateway',
|
||||
key: 'api-overview',
|
||||
url: `/project/${ref}/observability/api-overview${preservedQueryParams}`,
|
||||
},
|
||||
{
|
||||
name: 'Query Performance',
|
||||
key: 'query-performance',
|
||||
url: `/project/${ref}/observability/query-performance${preservedQueryParams}`,
|
||||
},
|
||||
...(postgrestReportEnabled
|
||||
? [
|
||||
{
|
||||
name: 'Data API',
|
||||
key: 'postgrest',
|
||||
url: `/project/${ref}/observability/postgrest${preservedQueryParams}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Product Reports',
|
||||
key: 'product-reports',
|
||||
items: [
|
||||
...(authEnabled
|
||||
? [
|
||||
{
|
||||
name: 'Auth',
|
||||
key: 'auth',
|
||||
url: `/project/${ref}/observability/auth${preservedQueryParams}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Database',
|
||||
key: 'database',
|
||||
url: `/project/${ref}/observability/database${preservedQueryParams}`,
|
||||
},
|
||||
...(edgeFnEnabled
|
||||
? [
|
||||
{
|
||||
name: 'Edge Functions',
|
||||
key: 'edge-functions',
|
||||
url: `/project/${ref}/observability/edge-functions${preservedQueryParams}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(realtimeEnabled
|
||||
? [
|
||||
{
|
||||
name: 'Realtime',
|
||||
key: 'realtime',
|
||||
url: `/project/${ref}/observability/realtime${preservedQueryParams}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(storageEnabled
|
||||
? [
|
||||
{
|
||||
name: 'Storage',
|
||||
key: 'storage',
|
||||
url: `/project/${ref}/observability/storage${preservedQueryParams}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Menu type="pills" className="mt-6">
|
||||
{isLoading ? (
|
||||
<div className="px-5 my-4 space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="mx-2">
|
||||
<Menu.Group
|
||||
title={
|
||||
<span className="flex w-full items-center justify-between relative h-6">
|
||||
<span className="uppercase font-mono">Custom Reports</span>
|
||||
{reportMenuItems.length > 0 && (
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<Plus />}
|
||||
disabled={!canCreateCustomReport}
|
||||
className="flex items-center justify-center h-6 w-6 absolute top-0 -right-1"
|
||||
onClick={() => {
|
||||
setShowNewReportModal(true)
|
||||
}}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canCreateCustomReport
|
||||
? 'You need additional permissions to create custom reports'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{reportMenuItems.length === 0 ? (
|
||||
<div className="px-2">
|
||||
<InnerSideBarEmptyPanel
|
||||
title="No custom reports yet"
|
||||
description="Create and save custom reports to track your project metrics"
|
||||
actions={
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
icon={<Plus />}
|
||||
disabled={!canCreateCustomReport}
|
||||
onClick={() => {
|
||||
setShowNewReportModal(true)
|
||||
}}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canCreateCustomReport
|
||||
? 'You need additional permissions to create custom reports'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
New custom report
|
||||
</ButtonTooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{reportMenuItems.map((item) => (
|
||||
<ObservabilityMenuItem
|
||||
key={item.id}
|
||||
item={item as any}
|
||||
pageKey={pageKey}
|
||||
onSelectEdit={() => {
|
||||
setSelectedReportToUpdate(item.report)
|
||||
}}
|
||||
onSelectDelete={() => {
|
||||
setSelectedReportToDelete(item.report)
|
||||
setDeleteModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{menuItems.map((item, idx) => (
|
||||
<>
|
||||
<div className="h-px w-full bg-border-overlay first:hidden" />
|
||||
<div key={item.key + '-menu-group'}>
|
||||
{item.items ? (
|
||||
<div className="px-2">
|
||||
<Menu.Group title={<span className="uppercase font-mono">{item.title}</span>} />
|
||||
<div key={item.key} className="flex flex-col">
|
||||
{item.items.map((subItem) => (
|
||||
<li
|
||||
key={subItem.key}
|
||||
className={cn(
|
||||
'pr-2 mt-1 text-foreground-light group-hover:text-foreground/80 text-sm',
|
||||
'flex items-center justify-between rounded-md group relative',
|
||||
subItem.key === pageKey
|
||||
? 'bg-surface-300 text-foreground'
|
||||
: 'hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href={subItem.url}
|
||||
className="flex-grow h-7 flex justify-between items-center pl-3"
|
||||
>
|
||||
<span>{subItem.name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
|
||||
<UpdateCustomReportModal
|
||||
onCancel={() => setSelectedReportToUpdate(undefined)}
|
||||
selectedReport={selectedReportToUpdate}
|
||||
initialValues={{
|
||||
name: selectedReportToUpdate?.name || '',
|
||||
description: selectedReportToUpdate?.description || '',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
title="Delete custom report"
|
||||
confirmLabel="Delete report"
|
||||
confirmLabelLoading="Deleting report"
|
||||
size="medium"
|
||||
loading={isDeleting}
|
||||
visible={deleteModalOpen}
|
||||
onCancel={() => setDeleteModalOpen(false)}
|
||||
onConfirm={onConfirmDeleteReport}
|
||||
>
|
||||
<div className="text-sm text-foreground-light grid gap-4">
|
||||
<div className="grid gap-1">
|
||||
<p>Are you sure you want to delete '{selectedReportToDelete?.name}'?</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<CreateReportModal
|
||||
visible={showNewReportModal}
|
||||
onCancel={() => setShowNewReportModal(false)}
|
||||
afterSubmit={() => setShowNewReportModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default ObservabilityMenu
|
||||
@@ -0,0 +1,111 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Edit2, Trash, MoreVertical, MoreHorizontal } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { ContentBase } from 'data/content/content-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import type { Dashboards } from 'types'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
|
||||
interface ReportMenuItemProps {
|
||||
item: {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
key: string
|
||||
url: string
|
||||
hasDropdownActions: boolean
|
||||
report: ContentBase & {
|
||||
type: 'report'
|
||||
content: Dashboards.Content
|
||||
}
|
||||
}
|
||||
pageKey: string
|
||||
onSelectEdit: () => void
|
||||
onSelectDelete: () => void
|
||||
}
|
||||
|
||||
export const ObservabilityMenuItem = ({
|
||||
item,
|
||||
pageKey,
|
||||
onSelectEdit,
|
||||
onSelectDelete,
|
||||
}: ReportMenuItemProps) => {
|
||||
const { profile } = useProfile()
|
||||
const { can: canUpdateCustomReport } = useAsyncCheckPermissions(
|
||||
PermissionAction.UPDATE,
|
||||
'user_content',
|
||||
{
|
||||
resource: {
|
||||
type: 'report',
|
||||
visibility: item.report.visibility,
|
||||
owner_id: item.report.owner_id,
|
||||
},
|
||||
subject: { id: profile?.id },
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
'pr-2 h-7 pl-3 mt-1 text-foreground-light group-hover:text-foreground/80 text-sm',
|
||||
'flex items-center justify-between rounded-md group relative',
|
||||
item.key === pageKey ? 'bg-surface-300 text-foreground' : 'hover:text-foreground'
|
||||
)}
|
||||
key={item.key + '-menukey'}
|
||||
href={item.url}
|
||||
>
|
||||
<div>{item.name}</div>
|
||||
|
||||
{canUpdateCustomReport && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1 opacity-50 hover:opacity-100"
|
||||
icon={<MoreVertical size={12} strokeWidth={2} />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-32 *:gap-x-2">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!item.id) return
|
||||
onSelectEdit()
|
||||
}}
|
||||
>
|
||||
<Edit2 size={12} />
|
||||
<div>Rename report</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!item.id) return
|
||||
onSelectDelete()
|
||||
}}
|
||||
>
|
||||
<Trash size={12} />
|
||||
<div>Delete report</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Blocks, FileText, Lightbulb, List, Settings } from 'lucide-react'
|
||||
import { Blocks, FileText, Lightbulb, List, Settings, Telescope } from 'lucide-react'
|
||||
|
||||
import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar'
|
||||
import { generateAuthMenu } from 'components/layouts/AuthLayout/AuthLayout.utils'
|
||||
@@ -152,10 +152,10 @@ export const generateOtherRoutes = (
|
||||
...(IS_PLATFORM && reportsEnabled
|
||||
? [
|
||||
{
|
||||
key: 'reports',
|
||||
label: 'Reports',
|
||||
icon: <Reports size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
||||
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/reports`),
|
||||
key: 'observability',
|
||||
label: 'Observability',
|
||||
icon: <Telescope size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
||||
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/observability`),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { useChartHighlight } from './useChartHighlight'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import type { UpdateDateRange } from 'pages/project/[ref]/reports/database'
|
||||
import type { UpdateDateRange } from 'pages/project/[ref]/observability/database'
|
||||
import type { ChartData } from './Charts.types'
|
||||
import { MultiAttribute } from './ComposedChart.utils'
|
||||
|
||||
|
||||
@@ -322,20 +322,30 @@ const nextConfig = {
|
||||
source: '/project/:ref/reports/linter',
|
||||
destination: '/project/:ref/database/linter',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/project/:ref/reports',
|
||||
destination: '/project/:ref/observability',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/project/:ref/reports/:path*',
|
||||
destination: '/project/:ref/observability/:path*',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/project/:ref/query-performance',
|
||||
destination: '/project/:ref/reports/query-performance',
|
||||
destination: '/project/:ref/observability/query-performance',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/project/:ref/advisors/query-performance',
|
||||
destination: '/project/:ref/reports/query-performance',
|
||||
destination: '/project/:ref/observability/query-performance',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/project/:ref/database/query-performance',
|
||||
destination: '/project/:ref/reports/query-performance',
|
||||
destination: '/project/:ref/observability/query-performance',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
|
||||
import Reports from 'components/interfaces/Reports/Reports'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const PageLayout: NextPageWithLayout = () => (
|
||||
@@ -14,7 +14,7 @@ const PageLayout: NextPageWithLayout = () => (
|
||||
|
||||
PageLayout.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout>{page}</ReportsLayout>
|
||||
<ObservabilityLayout>{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ import {
|
||||
import { DatePickerValue } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import { useApiReport } from 'data/reports/api-report-query'
|
||||
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
@@ -134,7 +134,7 @@ export const ApiReport: NextPageWithLayout = () => {
|
||||
|
||||
ApiReport.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout>{page}</ReportsLayout>
|
||||
<ObservabilityLayout>{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import { ReportChartV2 } from 'components/interfaces/Reports/v2/ReportChartV2'
|
||||
import { ReportSectionHeader } from 'components/interfaces/Reports/v2/ReportSectionHeader'
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
|
||||
import ReportFilterBar from 'components/interfaces/Reports/ReportFilterBar'
|
||||
@@ -49,7 +49,7 @@ const AuthReport: NextPageWithLayout = () => {
|
||||
|
||||
AuthReport.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout title="Auth">{page}</ReportsLayout>
|
||||
<ObservabilityLayout title="Auth">{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
+7
-4
@@ -3,7 +3,7 @@ import { useQueryClient } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowRight, ExternalLink, RefreshCw } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
@@ -18,7 +18,7 @@ import DiskSizeConfigurationModal from 'components/interfaces/Settings/Database/
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import ChartHandler from 'components/ui/Charts/ChartHandler'
|
||||
@@ -56,7 +56,7 @@ const DatabaseReport: NextPageWithLayout = () => {
|
||||
|
||||
DatabaseReport.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout title="Database">{page}</ReportsLayout>
|
||||
<ObservabilityLayout title="Database">{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
@@ -173,8 +173,11 @@ const DatabaseUsage = () => {
|
||||
setTimeout(() => setIsRefreshing(false), 1000)
|
||||
}
|
||||
|
||||
// [Joshen] Empty dependency array as we only want this running once
|
||||
const stateSyncedFromUrlRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (stateSyncedFromUrlRef.current) return
|
||||
stateSyncedFromUrlRef.current = true
|
||||
|
||||
if (db !== undefined) {
|
||||
setTimeout(() => {
|
||||
// [Joshen] Adding a timeout here to support navigation from settings to reports
|
||||
+2
-2
@@ -18,7 +18,7 @@ import {
|
||||
} from 'components/interfaces/Reports/v2/ReportsSelectFilter'
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useChartHoverState } from 'components/ui/Charts/useChartHoverState'
|
||||
|
||||
@@ -45,7 +45,7 @@ const EdgeFunctionsReportV2: NextPageWithLayout = () => {
|
||||
|
||||
EdgeFunctionsReportV2.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout title="Edge Functions">{page}</ReportsLayout>
|
||||
<ObservabilityLayout title="Edge Functions">{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
+5
-5
@@ -5,7 +5,7 @@ import { useState } from 'react'
|
||||
import { useParams } from 'common'
|
||||
import { CreateReportModal } from 'components/interfaces/Reports/CreateReportModal'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
import { useContentQuery } from 'data/content/content-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
@@ -30,8 +30,8 @@ export const UserReportPage: NextPageWithLayout = () => {
|
||||
const reports = data.content
|
||||
.filter((x) => x.type === 'report')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
if (reports.length >= 1) router.push(`/project/${ref}/reports/${reports[0].id}`)
|
||||
if (reports.length === 0) router.push(`/project/${ref}/reports/api-overview`)
|
||||
if (reports.length >= 1) router.push(`/project/${ref}/observability/${reports[0].id}`)
|
||||
if (reports.length === 0) router.push(`/project/${ref}/observability/api-overview`)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -52,7 +52,7 @@ export const UserReportPage: NextPageWithLayout = () => {
|
||||
) : (
|
||||
<>
|
||||
<ProductEmptyState
|
||||
title="Reports"
|
||||
title="Observability"
|
||||
ctaButtonLabel="New custom report"
|
||||
onClickCta={() => {
|
||||
setShowCreateReportModal(true)
|
||||
@@ -81,7 +81,7 @@ export const UserReportPage: NextPageWithLayout = () => {
|
||||
|
||||
UserReportPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout>{page}</ReportsLayout>
|
||||
<ObservabilityLayout>{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
+8
-5
@@ -1,7 +1,7 @@
|
||||
import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowRight, RefreshCw } from 'lucide-react'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
@@ -33,7 +33,7 @@ const PostgRESTReport: NextPageWithLayout = () => {
|
||||
|
||||
PostgRESTReport.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout title="PostgREST">{page}</ReportsLayout>
|
||||
<ObservabilityLayout title="PostgREST">{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
@@ -70,8 +70,11 @@ const PostgrestReport = () => {
|
||||
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
|
||||
// [Joshen] Empty dependency array as we only want this running once
|
||||
const hasStateSyncedFromUrlRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (hasStateSyncedFromUrlRef.current) return
|
||||
hasStateSyncedFromUrlRef.current = true
|
||||
|
||||
if (db !== undefined) {
|
||||
setTimeout(() => {
|
||||
// [Joshen] Adding a timeout here to support navigation from settings to reports
|
||||
@@ -86,7 +89,7 @@ const PostgrestReport = () => {
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}, 200)
|
||||
}
|
||||
}, [])
|
||||
}, [state, db, chart])
|
||||
|
||||
const handleDatePickerChange = (values: DatePickerValue) => {
|
||||
handleDatePickerChangeFromHook(values)
|
||||
+2
-2
@@ -14,7 +14,7 @@ import { Presets } from 'components/interfaces/Reports/Reports.types'
|
||||
import { queriesFactory } from 'components/interfaces/Reports/Reports.utils'
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import DatabaseSelector from 'components/ui/DatabaseSelector'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import { FormHeader } from 'components/ui/Forms/FormHeader'
|
||||
@@ -102,7 +102,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
|
||||
|
||||
QueryPerformanceReport.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout title="Query performance">{page}</ReportsLayout>
|
||||
<ObservabilityLayout title="Query performance">{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import ReportPadding from 'components/interfaces/Reports/ReportPadding'
|
||||
import { ReportChartV2 } from 'components/interfaces/Reports/v2/ReportChartV2'
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
|
||||
@@ -34,7 +34,7 @@ const RealtimeReport: NextPageWithLayout = () => {
|
||||
|
||||
RealtimeReport.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout title="Realtime">{page}</ReportsLayout>
|
||||
<ObservabilityLayout title="Realtime">{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ import {
|
||||
} from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useStorageReport } from 'data/reports/storage-report-query'
|
||||
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
|
||||
@@ -186,7 +186,7 @@ export const StorageReport: NextPageWithLayout = () => {
|
||||
|
||||
StorageReport.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ReportsLayout>{page}</ReportsLayout>
|
||||
<ObservabilityLayout>{page}</ObservabilityLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import { test } from 'vitest'
|
||||
|
||||
import { ApiReport } from 'pages/project/[ref]/reports/api-overview'
|
||||
import { ApiReport } from 'pages/project/[ref]/observability/api-overview'
|
||||
import { customRender as render } from 'tests/lib/custom-render'
|
||||
|
||||
test(`Render static elements`, async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import { test } from 'vitest'
|
||||
|
||||
import { StorageReport } from 'pages/project/[ref]/reports/storage'
|
||||
import { StorageReport } from 'pages/project/[ref]/observability/storage'
|
||||
import { render } from 'tests/helpers'
|
||||
|
||||
// [Joshen] Mock data for ApiReport is in __mocks__/hooks/useStorageReport
|
||||
|
||||
Reference in new issue
Block a user