ReportsMenu.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { useParams } from 'common'
  3. import { Plus } from 'lucide-react'
  4. import { useRouter } from 'next/router'
  5. import { parseAsBoolean, useQueryState } from 'nuqs'
  6. import { useMemo, useState } from 'react'
  7. import { toast } from 'sonner'
  8. import { Menu } from 'ui'
  9. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  10. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  11. import { ReportMenuItem } from './ReportMenuItem'
  12. import { CreateReportModal } from '@/components/interfaces/Reports/CreateReportModal'
  13. import { UpdateCustomReportModal } from '@/components/interfaces/Reports/UpdateModal'
  14. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  15. import { ProductMenu } from '@/components/ui/ProductMenu'
  16. import { useContentDeleteMutation } from '@/data/content/content-delete-mutation'
  17. import { Content, useContentQuery } from '@/data/content/content-query'
  18. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  19. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  20. import { useProfile } from '@/lib/profile'
  21. const ReportsMenu = () => {
  22. const router = useRouter()
  23. const { profile } = useProfile()
  24. const { ref, id } = useParams()
  25. const pageKey = (id || router.pathname.split('/')[4]) as string
  26. const storageSupported = useIsFeatureEnabled('project_storage:all')
  27. const { can: canCreateCustomReport } = useAsyncCheckPermissions(
  28. PermissionAction.CREATE,
  29. 'user_content',
  30. {
  31. resource: { type: 'report', owner_id: profile?.id },
  32. subject: { id: profile?.id },
  33. }
  34. )
  35. // Preserve date range query parameters when navigating
  36. const preservedQueryParams = useMemo(() => {
  37. const { its, ite, isHelper, helperText } = router.query
  38. const params = new URLSearchParams()
  39. if (its && typeof its === 'string') params.set('its', its)
  40. if (ite && typeof ite === 'string') params.set('ite', ite)
  41. if (isHelper && typeof isHelper === 'string') params.set('isHelper', isHelper)
  42. if (helperText && typeof helperText === 'string') params.set('helperText', helperText)
  43. const queryString = params.toString()
  44. return queryString ? `?${queryString}` : ''
  45. }, [router.query])
  46. const { data: content, isPending: isLoading } = useContentQuery({
  47. projectRef: ref,
  48. type: 'report',
  49. })
  50. const { mutate: deleteReport, isPending: isDeleting } = useContentDeleteMutation({
  51. onSuccess: () => {
  52. setDeleteModalOpen(false)
  53. toast.success('Successfully deleted report')
  54. router.push(`/project/${ref}/reports`)
  55. },
  56. onError: (error) => {
  57. toast.error(`Failed to delete report: ${error.message}`)
  58. },
  59. })
  60. const [deleteModalOpen, setDeleteModalOpen] = useState(false)
  61. const [showNewReportModal, setShowNewReportModal] = useQueryState(
  62. 'newReport',
  63. parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
  64. )
  65. const [selectedReportToDelete, setSelectedReportToDelete] = useState<Content>()
  66. const [selectedReportToUpdate, setSelectedReportToUpdate] = useState<Content>()
  67. const onConfirmDeleteReport = () => {
  68. if (ref === undefined) return console.error('Project ref is required')
  69. if (selectedReportToDelete?.id === undefined) return console.error('Report ID is required')
  70. deleteReport({ projectRef: ref, ids: [selectedReportToDelete.id] })
  71. }
  72. function getReportMenuItems() {
  73. if (!content) return []
  74. const reports = content?.content.filter((c) => c.type === 'report')
  75. const sortedReports = reports?.sort((a, b) => {
  76. if (a.name < b.name) {
  77. return -1
  78. }
  79. if (a.name > b.name) {
  80. return 1
  81. }
  82. return 0
  83. })
  84. const reportMenuItems = sortedReports.map((r, idx) => ({
  85. id: r.id,
  86. name: r.name,
  87. description: r.description || '',
  88. key: r.id || idx + '-report',
  89. url: `/project/${ref}/reports/${r.id}${preservedQueryParams}`,
  90. hasDropdownActions: true,
  91. report: r,
  92. }))
  93. return reportMenuItems
  94. }
  95. const reportMenuItems = getReportMenuItems()
  96. const menuItems = [
  97. {
  98. title: 'Built-in reports',
  99. key: 'builtin-reports',
  100. items: [
  101. {
  102. name: 'API Gateway',
  103. key: 'api-overview',
  104. url: `/project/${ref}/reports/api-overview${preservedQueryParams}`,
  105. },
  106. {
  107. name: 'Auth',
  108. key: 'auth',
  109. url: `/project/${ref}/reports/auth${preservedQueryParams}`,
  110. },
  111. {
  112. name: 'Database',
  113. key: 'database',
  114. url: `/project/${ref}/reports/database${preservedQueryParams}`,
  115. },
  116. {
  117. name: 'Edge Functions',
  118. key: 'edge-functions',
  119. url: `/project/${ref}/reports/edge-functions${preservedQueryParams}`,
  120. },
  121. {
  122. name: 'Query Performance',
  123. key: 'query-performance',
  124. url: `/project/${ref}/reports/query-performance${preservedQueryParams}`,
  125. },
  126. {
  127. name: 'PostgREST',
  128. key: 'postgrest',
  129. url: `/project/${ref}/reports/postgrest${preservedQueryParams}`,
  130. },
  131. {
  132. name: 'Realtime',
  133. key: 'realtime',
  134. url: `/project/${ref}/reports/realtime${preservedQueryParams}`,
  135. },
  136. ...(storageSupported
  137. ? [
  138. {
  139. name: 'Storage',
  140. key: 'storage',
  141. url: `/project/${ref}/reports/storage${preservedQueryParams}`,
  142. },
  143. ]
  144. : []),
  145. ],
  146. },
  147. ]
  148. return (
  149. <div className="mt-6">
  150. {isLoading ? (
  151. <div className="px-5 my-4 space-y-2">
  152. <ShimmeringLoader />
  153. <ShimmeringLoader className="w-3/4" />
  154. <ShimmeringLoader className="w-1/2" />
  155. </div>
  156. ) : (
  157. <div className="flex flex-col px-2 gap-y-6">
  158. <div className="px-2">
  159. <ButtonTooltip
  160. block
  161. type="default"
  162. icon={<Plus />}
  163. disabled={!canCreateCustomReport}
  164. className="justify-start grow"
  165. onClick={() => {
  166. setShowNewReportModal(true)
  167. }}
  168. tooltip={{
  169. content: {
  170. side: 'bottom',
  171. text: !canCreateCustomReport
  172. ? 'You need additional permissions to create custom reports'
  173. : undefined,
  174. },
  175. }}
  176. >
  177. New custom report
  178. </ButtonTooltip>
  179. </div>
  180. {reportMenuItems.length > 0 ? (
  181. <Menu type="pills">
  182. <Menu.Group
  183. title={<span className="uppercase font-mono">Your custom reports</span>}
  184. />
  185. {reportMenuItems.map((item) => (
  186. <ReportMenuItem
  187. key={item.id}
  188. item={item as any}
  189. pageKey={pageKey}
  190. onSelectEdit={() => {
  191. setSelectedReportToUpdate(item.report)
  192. }}
  193. onSelectDelete={() => {
  194. setSelectedReportToDelete(item.report)
  195. setDeleteModalOpen(true)
  196. }}
  197. />
  198. ))}
  199. </Menu>
  200. ) : null}
  201. <ProductMenu
  202. page={pageKey}
  203. menu={menuItems.map((item) => ({
  204. ...item,
  205. items: item.items.map((subItem) => ({ ...subItem, items: [] })),
  206. }))}
  207. />
  208. <UpdateCustomReportModal
  209. onCancel={() => setSelectedReportToUpdate(undefined)}
  210. selectedReport={selectedReportToUpdate}
  211. initialValues={{
  212. name: selectedReportToUpdate?.name || '',
  213. description: selectedReportToUpdate?.description || '',
  214. }}
  215. />
  216. <ConfirmationModal
  217. title="Delete custom report"
  218. confirmLabel="Delete report"
  219. confirmLabelLoading="Deleting report"
  220. size="medium"
  221. loading={isDeleting}
  222. visible={deleteModalOpen}
  223. onCancel={() => setDeleteModalOpen(false)}
  224. onConfirm={onConfirmDeleteReport}
  225. >
  226. <div className="text-sm text-foreground-light grid gap-4">
  227. <div className="grid gap-1">
  228. <p>Are you sure you want to delete '{selectedReportToDelete?.name}'?</p>
  229. </div>
  230. </div>
  231. </ConfirmationModal>
  232. <CreateReportModal
  233. visible={showNewReportModal}
  234. onCancel={() => setShowNewReportModal(false)}
  235. afterSubmit={() => setShowNewReportModal(false)}
  236. />
  237. </div>
  238. )}
  239. </div>
  240. )
  241. }
  242. export default ReportsMenu