FeaturePreviewModal.tsx250 lines · main
| 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 | |
| 26 | import { AdvisorRulesPreview } from './AdvisorRulesPreview' |
| 27 | import { CLSPreview } from './CLSPreview' |
| 28 | import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext' |
| 29 | import { JitDbAccessPreview } from './JitDbAccessPreview' |
| 30 | import { MarketplacePreview } from './MarketplacePreview' |
| 31 | import { PgDeltaDiffPreview } from './PgDeltaDiffPreview' |
| 32 | import { PlatformWebhooksPreview } from './PlatformWebhooksPreview' |
| 33 | import { RLSTesterPreview } from './RLSTesterPreview' |
| 34 | import { UnifiedLogsPreview } from './UnifiedLogsPreview' |
| 35 | import { FeaturePreview, useFeaturePreviews } from './useFeaturePreviews' |
| 36 | import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider' |
| 37 | import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' |
| 38 | import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' |
| 39 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 40 | import { IS_PLATFORM } from '@/lib/constants' |
| 41 | |
| 42 | const 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 | |
| 55 | export 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 | |
| 211 | const 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 | } |