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