FeaturePreviewModal.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. // @ts-nocheck
  2. import { LOCAL_STORAGE_KEYS, useParams } from 'common'
  3. import { ExternalLink, Eye, EyeOff, FlaskConical } from 'lucide-react'
  4. import Link from 'next/link'
  5. import { ReactNode } from 'react'
  6. import {
  7. Badge,
  8. Button,
  9. cn,
  10. Dialog,
  11. DialogContent,
  12. DialogDescription,
  13. DialogHeader,
  14. DialogSection,
  15. DialogSectionSeparator,
  16. DialogTitle,
  17. ScrollArea,
  18. } from 'ui'
  19. import {
  20. Select,
  21. SelectContent,
  22. SelectItem,
  23. SelectTrigger,
  24. } from 'ui/src/components/shadcn/ui/select'
  25. import { AdvisorRulesPreview } from './AdvisorRulesPreview'
  26. import { CLSPreview } from './CLSPreview'
  27. import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext'
  28. import { JitDbAccessPreview } from './JitDbAccessPreview'
  29. import { MarketplacePreview } from './MarketplacePreview'
  30. import { PgDeltaDiffPreview } from './PgDeltaDiffPreview'
  31. import { PlatformWebhooksPreview } from './PlatformWebhooksPreview'
  32. import { RLSTesterPreview } from './RLSTesterPreview'
  33. import { UnifiedLogsPreview } from './UnifiedLogsPreview'
  34. import { FeaturePreview, useFeaturePreviews } from './useFeaturePreviews'
  35. import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
  36. import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
  37. import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
  38. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  39. import { IS_PLATFORM } from '@/lib/constants'
  40. const FEATURE_PREVIEW_KEY_TO_CONTENT: {
  41. [key: string]: ReactNode
  42. } = {
  43. [LOCAL_STORAGE_KEYS.UI_PREVIEW_PG_DELTA_DIFF]: <PgDeltaDiffPreview />,
  44. [LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES]: <AdvisorRulesPreview />,
  45. [LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: <CLSPreview />,
  46. [LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]: <UnifiedLogsPreview />,
  47. [LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: <PlatformWebhooksPreview />,
  48. [LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: <JitDbAccessPreview />,
  49. [LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]: <RLSTesterPreview />,
  50. [LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: <MarketplacePreview />,
  51. }
  52. export const FeaturePreviewModal = () => {
  53. const { ref } = useParams()
  54. const featurePreviews = useFeaturePreviews()
  55. const {
  56. showFeaturePreviewModal,
  57. selectedFeatureKey,
  58. selectFeaturePreview,
  59. toggleFeaturePreviewModal,
  60. } = useFeaturePreviewModal()
  61. const { data: org } = useSelectedOrganizationQuery()
  62. const featurePreviewContext = useFeaturePreviewContext()
  63. const { mutate: sendEvent } = useSendEventMutation()
  64. const { dismissBanner } = useBannerStack()
  65. const [, setIsDismissedRlsTesterBanner] = useLocalStorageQuery(
  66. LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(ref ?? ''),
  67. false
  68. )
  69. const { flags, onUpdateFlag } = featurePreviewContext
  70. const allFeaturePreviews = (
  71. IS_PLATFORM ? featurePreviews : featurePreviews.filter((x) => !x.isPlatformOnly)
  72. ).filter((x) => x.enabled)
  73. const selectedFeature =
  74. allFeaturePreviews.find((preview) => preview.key === selectedFeatureKey) ??
  75. allFeaturePreviews[0]
  76. const isSelectedFeatureEnabled = flags[selectedFeature?.key]
  77. const toggleFeature = () => {
  78. if (!selectedFeature) return
  79. if (selectedFeature.key === LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER) {
  80. dismissBanner('rls-tester-banner')
  81. setIsDismissedRlsTesterBanner(true)
  82. }
  83. onUpdateFlag(selectedFeature.key, !isSelectedFeatureEnabled)
  84. sendEvent({
  85. action: isSelectedFeatureEnabled ? 'feature_preview_disabled' : 'feature_preview_enabled',
  86. properties: { feature: selectedFeature.key },
  87. groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
  88. })
  89. }
  90. return (
  91. <Dialog open={showFeaturePreviewModal} onOpenChange={toggleFeaturePreviewModal}>
  92. <DialogContent size="xlarge" className="flex flex-col max-w-4xl! h-[90dvh] md:h-auto">
  93. <DialogHeader>
  94. <DialogTitle>Dashboard feature previews</DialogTitle>
  95. <DialogDescription>Get early access to new features and give feedback</DialogDescription>
  96. </DialogHeader>
  97. <DialogSectionSeparator />
  98. <DialogSection className="p-0! flex-1 min-h-0 h-full">
  99. {allFeaturePreviews.length > 0 ? (
  100. <div className="max-h-full flex-1 min-h-0 h-full flex flex-col gap-y-1 md:gap-y-4 md:flex-row">
  101. <div>
  102. <ScrollArea className="hidden md:block h-[550px] w-[280px] border-r">
  103. {allFeaturePreviews.map((feature) => (
  104. <FeaturePreviewItem
  105. key={feature.key}
  106. feature={feature}
  107. selectedFeature={selectedFeature}
  108. selectFeaturePreview={selectFeaturePreview}
  109. />
  110. ))}
  111. </ScrollArea>
  112. </div>
  113. <div className="block md:hidden px-4 pt-4">
  114. <Select
  115. value={selectedFeature.key}
  116. onValueChange={selectFeaturePreview}
  117. defaultValue={selectedFeature.name}
  118. >
  119. <SelectTrigger id="feature-preview-select">
  120. <div className="flex items-center gap-x-2">
  121. {(flags[selectedFeature.key] ?? false) ? (
  122. <Eye size={14} strokeWidth={2} className="text-brand" />
  123. ) : (
  124. <EyeOff size={14} strokeWidth={1.5} className="text-foreground-light" />
  125. )}
  126. <p>{selectedFeature.name}</p>
  127. {selectedFeature.isNew && <Badge variant="success">New</Badge>}
  128. </div>
  129. </SelectTrigger>
  130. <SelectContent className="p-0! [&>div]:w-full! [&>div]:p-0! [&>div]:flex! [&>div]:flex-col! w-full flex">
  131. {allFeaturePreviews.map((feature) => (
  132. <SelectItem
  133. key={feature.key}
  134. value={feature.key}
  135. className="p-0 [&>span:nth-child(2)]:w-full [&>span:nth-child(1)]:left-3"
  136. >
  137. <FeaturePreviewItem
  138. feature={feature}
  139. selectedFeature={selectedFeature}
  140. selectFeaturePreview={selectFeaturePreview}
  141. className="pl-10 py-3 bg-transparent"
  142. />
  143. </SelectItem>
  144. ))}
  145. </SelectContent>
  146. </Select>
  147. </div>
  148. <div className="h-auto min-h-0 max-h-auto md:max-h-[550px] p-4 pb-0 flex flex-col">
  149. <div className="flex items-center justify-between border-b gap-2 pb-3">
  150. <p>{selectedFeature?.name}</p>
  151. <div className="flex items-center gap-x-2">
  152. {selectedFeature?.discussionsUrl !== undefined && (
  153. <Button asChild type="default" icon={<ExternalLink strokeWidth={1.5} />}>
  154. <Link
  155. href={selectedFeature.discussionsUrl}
  156. target="_blank"
  157. rel="noreferrer"
  158. >
  159. Give feedback
  160. </Link>
  161. </Button>
  162. )}
  163. <Button type="default" onClick={() => toggleFeature()}>
  164. {isSelectedFeatureEnabled ? 'Disable' : 'Enable'} feature
  165. </Button>
  166. </div>
  167. </div>
  168. <div className="overflow-y-scroll pt-3 pb-4">
  169. {FEATURE_PREVIEW_KEY_TO_CONTENT[selectedFeature?.key ?? '']}
  170. </div>
  171. </div>
  172. </div>
  173. ) : (
  174. <div className="h-[550px] flex flex-col items-center justify-center">
  175. <FlaskConical size={30} strokeWidth={1.5} className="text-foreground-light" />
  176. <div className="mt-1 mb-3 flex flex-col items-center gap-y-0.5">
  177. <p className="text-sm">No feature previews available</p>
  178. <p className="text-sm text-foreground-light">
  179. Have an idea for the dashboard? Let us know via GitHub Discussions!
  180. </p>
  181. </div>
  182. <Button asChild type="default" icon={<ExternalLink strokeWidth={1.5} />}>
  183. <Link
  184. href="https://github.com/orgs/briven/discussions/categories/feature-requests"
  185. target="_blank"
  186. rel="noreferrer"
  187. >
  188. GitHub Discussions
  189. </Link>
  190. </Button>
  191. </div>
  192. )}
  193. </DialogSection>
  194. </DialogContent>
  195. </Dialog>
  196. )
  197. }
  198. const FeaturePreviewItem = ({
  199. feature,
  200. selectedFeature,
  201. selectFeaturePreview,
  202. className,
  203. }: {
  204. feature: FeaturePreview
  205. selectedFeature: FeaturePreview
  206. selectFeaturePreview: (key: string) => void
  207. className?: string
  208. }) => {
  209. const featurePreviewContext = useFeaturePreviewContext()
  210. const { flags } = featurePreviewContext
  211. const isEnabled = flags[feature.key] ?? false
  212. return (
  213. <button
  214. type="button"
  215. key={feature.key}
  216. onClick={() => selectFeaturePreview(feature.key)}
  217. className={cn(
  218. 'w-full! flex-1 flex items-center justify-between p-4 border-b cursor-pointer bg transition',
  219. selectedFeature?.key === feature.key ? 'bg-surface-300' : 'bg-surface-100',
  220. className
  221. )}
  222. >
  223. <div className="flex items-center gap-x-3">
  224. {isEnabled ? (
  225. <Eye size={14} strokeWidth={2} className="text-brand" />
  226. ) : (
  227. <EyeOff size={14} strokeWidth={1.5} className="text-foreground-light" />
  228. )}
  229. <p className="text-sm truncate" title={feature.name}>
  230. {feature.name}
  231. </p>
  232. </div>
  233. {feature.isNew && <Badge variant="success">New</Badge>}
  234. </button>
  235. )
  236. }