Feat/UI feature previews (#17775)

* Break me.tsx into smaller components

* Deprecate duplicate app valtio state

* Initialize feature preview modal

* Add telemetry

* Address comments
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-09-29 13:20:30 +08:00
1 parent bc1c9090c2
commit 32dda1bb23
15 files changed
+332 -137

No files matched your search

@@ -0,0 +1,57 @@
import { Button, Input } from 'ui'
import Link from 'next/link'
import Panel from 'components/ui/Panel'
import { useSession } from 'lib/auth'
import { Profile } from 'data/profile/types'
const AccountInformation = ({ profile }: { profile?: Profile }) => {
const session = useSession()
return (
<Panel
title={
<h5 key="panel-title" className="mb-0">
Account Information
</h5>
}
>
<Panel.Content>
<div className="space-y-2">
<Input
readOnly
disabled
label="Username"
layout="horizontal"
value={profile?.username ?? ''}
/>
<Input
readOnly
disabled
label="Email"
layout="horizontal"
value={profile?.primary_email ?? ''}
/>
{session?.user.app_metadata.provider === 'email' && (
<div className="text-sm grid gap-2 md:grid md:grid-cols-12 md:gap-x-4">
<div className="flex flex-col space-y-2 col-span-4 ">
<p className="text-foreground-light break-all">Password</p>
</div>
<div className="col-span-8">
<Link href="/reset-password">
<a>
<Button type="default" size="medium">
Reset password
</Button>
</a>
</Link>
</div>
</div>
)}
</div>
</Panel.Content>
</Panel>
)
}
export default AccountInformation
@@ -0,0 +1,28 @@
import { Toggle } from 'ui'
import Panel from 'components/ui/Panel'
import { useAppStateSnapshot } from 'state/app-state'
const AnalyticsSettings = () => {
const snap = useAppStateSnapshot()
const onToggleOptIn = () => {
const value = !snap.isOptedInTelemetry ? 'true' : 'false'
snap.setIsOptedInTelemetry(value === 'true')
}
return (
<Panel title={<h5 key="panel-title">Analytics</h5>}>
<Panel.Content>
<Toggle
checked={snap.isOptedInTelemetry}
onChange={onToggleOptIn}
label="Opt-in to send telemetry data from the dashboard"
descriptionText="By opting into sending telemetry data, Supabase can improve the overall dashboard user experience"
/>
</Panel.Content>
</Panel>
)
}
export default AnalyticsSettings
@@ -0,0 +1,36 @@
import { useTheme } from 'next-themes'
import { IconMoon, IconSun, Listbox } from 'ui'
import Panel from 'components/ui/Panel'
const ThemeSettings = () => {
const { theme, setTheme } = useTheme()
return (
<Panel title={<h5 key="panel-title">Theme</h5>}>
<Panel.Content>
<Listbox
value={theme}
label="Interface theme"
descriptionText="Choose a theme preference"
layout="horizontal"
style={{ width: '50%' }}
icon={theme ? <IconMoon /> : <IconSun />}
onChange={(value: string) => setTheme(value)}
>
<Listbox.Option label="System default" value="system">
System default
</Listbox.Option>
<Listbox.Option label="Dark" value="dark">
Dark
</Listbox.Option>
<Listbox.Option label="Light" value="light">
Light
</Listbox.Option>
</Listbox>
</Panel.Content>
</Panel>
)
}
export default ThemeSettings
@@ -0,0 +1,5 @@
import AccountInformation from './AccountInformation'
import AnalyticsSettings from './AnalyticsSettings'
import ThemeSettings from './ThemeSettings'
export { AccountInformation, AnalyticsSettings, ThemeSettings }
@@ -0,0 +1,61 @@
import { noop } from 'lodash'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { EMPTY_OBJ } from 'lib/void'
type FeaturePreviewContextType = {
flags: { [key: string]: boolean }
onUpdateFlag: (key: string, value: boolean) => void
}
const FeaturePreviewContext = createContext<FeaturePreviewContextType>({
flags: EMPTY_OBJ,
onUpdateFlag: noop,
})
export default FeaturePreviewContext
export const useFeaturePreviewContext = () => useContext(FeaturePreviewContext)
export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{}>) => {
const [flags, setFlags] = useState({
[LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT]: false,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]: false,
})
useEffect(() => {
if (typeof window !== 'undefined') {
setFlags({
[LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT]:
localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT) === 'true',
[LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]:
localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL) === 'true',
})
}
}, [])
const value = {
flags,
onUpdateFlag: (key: string, value: boolean) => {
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, value.toString())
}
const updatedFlags = { ...flags, [key]: value }
setFlags(updatedFlags)
},
}
return <FeaturePreviewContext.Provider value={value}>{children}</FeaturePreviewContext.Provider>
}
// Helpers
export const useIsNavigationPreviewEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT]
}
export const useIsAPIDocsSidePanelEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]
}
@@ -0,0 +1,99 @@
import { useTelemetryProps } from 'common'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { Button, IconEye, IconEyeOff, Modal, ScrollArea, cn } from 'ui'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import Telemetry from 'lib/telemetry'
import { useAppStateSnapshot } from 'state/app-state'
import { Markdown } from '../Markdown'
import { useFeaturePreviewContext } from './FeaturePreviewContext'
// [Ivan] We should probably move this to a separate file, together with LOCAL_STORAGE_KEYS. We should make adding new feature previews as simple as possible.
const FEATURE_PREVIEWS = [
// {
// key: LOCAL_STORAGE_KEYS.UI_PREVIEW_NAVIGATION_LAYOUT,
// name: 'Global navigation update',
// description: `Experience a redesigned and improved site navigation on the dashboard, with an intention to making finding your way around more intuitive and easier.`,
// },
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
name: 'Project API documentation',
description: `Get building with your client application faster with your Project's API documentation now available on any page across the dashboard.`,
},
]
const FeaturePreviewModal = () => {
const router = useRouter()
const snap = useAppStateSnapshot()
const telemetryProps = useTelemetryProps()
const featurePreviewContext = useFeaturePreviewContext()
const [selectedFeatureKey, setSelectedFeatureKey] = useState<string>(FEATURE_PREVIEWS[0].key)
const { flags, onUpdateFlag } = featurePreviewContext
const selectedFeature = FEATURE_PREVIEWS.find((preview) => preview.key === selectedFeatureKey)
const isSelectedFeatureEnabled = flags[selectedFeatureKey]
const toggleFeature = () => {
onUpdateFlag(selectedFeatureKey, !isSelectedFeatureEnabled)
Telemetry.sendEvent(
{
category: 'ui_feature_previews',
action: isSelectedFeatureEnabled ? 'disabled' : 'enabled',
label: selectedFeatureKey,
},
telemetryProps,
router
)
}
return (
<Modal
hideFooter
size="xlarge"
header="Dashboard feature previews"
visible={snap.showFeaturePreviewModal}
onCancel={() => snap.setShowFeaturePreviewModal(false)}
>
<div className="flex">
<div>
<ScrollArea className="h-[400px] w-[240px] border-r">
{FEATURE_PREVIEWS.map((feature) => {
const isEnabled = flags[feature.key] ?? false
return (
<div
key={feature.key}
onClick={() => setSelectedFeatureKey(feature.key)}
className={cn(
'flex items-center space-x-3 p-4 border-b cursor-pointer bg transition',
selectedFeatureKey === feature.key ? 'bg-surface-200' : ''
)}
>
{isEnabled ? (
<IconEye size={14} strokeWidth={2} className="text-brand" />
) : (
<IconEyeOff size={14} strokeWidth={1.5} className="text-foreground-light" />
)}
<p className="text-sm">{feature.name}</p>
</div>
)
})}
</ScrollArea>
</div>
<div className="flex-grow p-4 space-y-3">
<div className="flex items-center justify-between">
<p>{selectedFeature?.name}</p>
<Button type="default" onClick={() => toggleFeature()}>
{isSelectedFeatureEnabled ? 'Disable' : 'Enable'} feature
</Button>
</div>
<Markdown className="text-sm" content={selectedFeature?.description ?? ''} />
</div>
</div>
</Modal>
)
}
export default FeaturePreviewModal
@@ -10,8 +10,6 @@ import {
Button,
IconAlertTriangle,
IconGitBranch,
IconSearch,
Input,
Modal,
} from 'ui'
@@ -27,7 +25,7 @@ import { Branch, useBranchesQuery } from 'data/branches/branches-query'
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
import { useSelectedOrganization, useSelectedProject, useStore } from 'hooks'
import { OPT_IN_TAGS } from 'lib/constants'
import { useAppUiStateSnapshot } from 'state/app'
import { useAppStateSnapshot } from 'state/app-state'
import { MainBranchPanel } from './BranchPanels'
import CreateBranchSidePanel from './CreateBranchSidePanel'
import PreviewBranches from './PreviewBranches'
@@ -48,7 +46,7 @@ const BranchManagement = () => {
const projectRef =
project !== undefined ? (isBranch ? project.parent_project_ref : ref) : undefined
const snap = useAppUiStateSnapshot()
const snap = useAppStateSnapshot()
const [showCreateBranch, setShowCreateBranch] = useState(false)
const [showDisableBranching, setShowDisableBranching] = useState(false)
const [selectedBranchToDelete, setSelectedBranchToDelete] = useState<Branch>()
@@ -1,17 +1,17 @@
import { Button, IconGitBranch } from 'ui'
import { useSelectedOrganization } from 'hooks'
import { useAppUiStateSnapshot } from 'state/app'
import BranchingWaitlistPopover from './BranchingWaitlistPopover'
import { OPT_IN_TAGS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
interface EnableBranchingButtonProps {
isNewNav?: boolean
}
const EnableBranchingButton = ({ isNewNav = false }: EnableBranchingButtonProps) => {
const snap = useAppUiStateSnapshot()
const snap = useAppStateSnapshot()
const selectedOrg = useSelectedOrganization()
const hasAccessToBranching =
@@ -9,17 +9,15 @@ import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
import { IntegrationName } from 'data/integrations/integrations.types'
import { useSelectedOrganization, useStore } from 'hooks'
import { useAppUiStateSnapshot } from 'state/app'
import BranchingWaitlistPopover from './BranchingWaitlistPopover'
import { useAppStateSnapshot } from 'state/app-state'
import GithubRepositorySelection from './GithubRepositorySelection'
import VercelProjectSelection from './VercelProjectSelection'
const EnableBranchingModal = () => {
const { ui } = useStore()
const { ref } = useParams()
const selectedOrg = useSelectedOrganization()
const snap = useAppUiStateSnapshot()
const snap = useAppStateSnapshot()
const [selectedBranch, setSelectedBranch] = useState<string>()
const [addConnectionType, setAddConnectionType] = useState<IntegrationName>()
@@ -4,6 +4,7 @@ import { useFlag } from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
import { detectOS } from 'lib/helpers'
import { isUndefined } from 'lodash'
import { FlaskConical } from 'lucide-react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useRouter } from 'next/router'
@@ -32,23 +33,26 @@ import {
generateToolRoutes,
} from './NavigationBar.utils'
import NavigationIconButton from './NavigationIconButton'
import ThemeToggle from '@ui/components/ThemeProvider/ThemeToggle'
import { useAppStateSnapshot } from 'state/app-state'
const NavigationBar = () => {
const os = detectOS()
const router = useRouter()
const snap = useAppStateSnapshot()
const { theme, setTheme } = useTheme()
const { ref: projectRef } = useParams()
const os = detectOS()
const { setIsOpen } = useCommandMenu()
const { project } = useProjectContext()
const navLayoutV2 = useFlag('navigationLayoutV2')
const supabaseAIEnabled = useFlag('sqlEditorSupabaseAI')
const showFeaturePreviews = useFlag('featurePreviews')
const activeRoute = router.pathname.split('/')[3]
const toolRoutes = generateToolRoutes(projectRef, project, supabaseAIEnabled)
const productRoutes = generateProductRoutes(projectRef, project)
const otherRoutes = generateOtherRoutes(projectRef, project)
return (
<div
className={[
@@ -153,9 +157,20 @@ const NavigationBar = () => {
<Link href="/account/me">
<DropdownMenuItem_Shadcn_ key="header" className="space-x-2">
<IconSettings size={14} strokeWidth={1.5} />
<p className="text">Account Preferences</p>
<p className="text">Account preferences</p>
</DropdownMenuItem_Shadcn_>
</Link>
{showFeaturePreviews && (
<DropdownMenuItem_Shadcn_
key="header"
className="space-x-2"
onClick={() => snap.setShowFeaturePreviewModal(true)}
onSelect={() => snap.setShowFeaturePreviewModal(true)}
>
<FlaskConical size={14} strokeWidth={2} />
<p className="text">Feature previews</p>
</DropdownMenuItem_Shadcn_>
)}
<DropdownMenuSeparator_Shadcn_ />
</>
)}
+2
View File
@@ -96,6 +96,8 @@ export const USAGE_APPROACHING_THRESHOLD = 0.75
export const LOCAL_STORAGE_KEYS = {
RECENTLY_VISITED_ORGANIZATION: 'supabase-organization',
TELEMETRY_CONSENT: 'supabase-consent',
UI_PREVIEW_NAVIGATION_LAYOUT: 'supabase-ui-preview-nav-layout',
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
}
export const OPT_IN_TAGS = {
+6 -1
View File
@@ -55,6 +55,8 @@ import { useAppStateSnapshot } from 'state/app-state'
import { RootStore } from 'stores'
import HCaptchaLoadedStore from 'stores/hcaptcha-loaded-store'
import { AppPropsWithLayout } from 'types'
import { FeaturePreviewContextProvider } from 'components/interfaces/App/FeaturePreviewContext'
import FeaturePreviewModal from 'components/interfaces/App/FeaturePreviewModal'
dayjs.extend(customParseFormat)
dayjs.extend(utc)
@@ -192,7 +194,10 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
>
<CommandMenuWrapper>
<AppBannerWrapper>
{getLayout(<Component {...pageProps} />)}
<FeaturePreviewContextProvider>
{getLayout(<Component {...pageProps} />)}
<FeaturePreviewModal />
</FeaturePreviewContextProvider>
</AppBannerWrapper>
</CommandMenuWrapper>
</ThemeProvider>
+6 -109
View File
@@ -1,21 +1,19 @@
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { useTheme } from 'next-themes'
import {
AccountInformation,
AnalyticsSettings,
ThemeSettings,
} from 'components/interfaces/Account/Preferences'
import { AccountLayout } from 'components/layouts'
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
import AlertError from 'components/ui/AlertError'
import Panel from 'components/ui/Panel'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useProfileUpdateMutation } from 'data/profile/profile-update-mutation'
import { Profile as ProfileType } from 'data/profile/types'
import { useStore } from 'hooks'
import { useSession } from 'lib/auth'
import { useProfile } from 'lib/profile'
import { useAppStateSnapshot } from 'state/app-state'
import { NextPageWithLayout } from 'types'
import { Button, IconMoon, IconSun, Input, Listbox, Toggle } from 'ui'
const User: NextPageWithLayout = () => {
return (
@@ -86,7 +84,7 @@ const ProfileCard = observer(() => {
{isSuccess && (
<>
<section>
<Profile profile={profile} />
<AccountInformation profile={profile} />
</section>
<section>
{/* @ts-ignore */}
@@ -121,104 +119,3 @@ const ProfileCard = observer(() => {
</article>
)
})
const Profile = ({ profile }: { profile?: ProfileType }) => {
const session = useSession()
return (
<Panel
title={
<h5 key="panel-title" className="mb-0">
Account Information
</h5>
}
>
<Panel.Content>
<div className="space-y-2">
<Input
readOnly
disabled
label="Username"
layout="horizontal"
value={profile?.username ?? ''}
/>
<Input
readOnly
disabled
label="Email"
layout="horizontal"
value={profile?.primary_email ?? ''}
/>
{session?.user.app_metadata.provider === 'email' && (
<div className="text-sm grid gap-2 md:grid md:grid-cols-12 md:gap-x-4">
<div className="flex flex-col space-y-2 col-span-4 ">
<p className="text-foreground-light break-all">Password</p>
</div>
<div className="col-span-8">
<Link href="/reset-password">
<a>
<Button type="default" size="medium">
Reset password
</Button>
</a>
</Link>
</div>
</div>
)}
</div>
</Panel.Content>
</Panel>
)
}
const ThemeSettings = observer(() => {
const { theme, setTheme } = useTheme()
return (
<Panel title={<h5 key="panel-title">Theme</h5>}>
<Panel.Content>
<Listbox
value={theme}
label="Interface theme"
descriptionText="Choose a theme preference"
layout="horizontal"
style={{ width: '50%' }}
icon={theme ? <IconMoon /> : <IconSun />}
onChange={(value: string) => setTheme(value)}
>
<Listbox.Option label="System default" value="system">
System default
</Listbox.Option>
<Listbox.Option label="Dark" value="dark">
Dark
</Listbox.Option>
<Listbox.Option label="Light" value="light">
Light
</Listbox.Option>
</Listbox>
</Panel.Content>
</Panel>
)
})
const AnalyticsSettings = observer(() => {
const snap = useAppStateSnapshot()
const onToggleOptIn = () => {
const value = !snap.isOptedInTelemetry ? 'true' : 'false'
snap.setIsOptedInTelemetry(value === 'true')
}
return (
<Panel title={<h5 key="panel-title">Analytics</h5>}>
<Panel.Content>
<Toggle
checked={snap.isOptedInTelemetry}
onChange={onToggleOptIn}
label="Opt-in to send telemetry data from the dashboard"
descriptionText="By opting into sending telemetry data, Supabase can improve the overall dashboard user experience"
/>
</Panel.Content>
</Panel>
)
})
+8
View File
@@ -9,6 +9,14 @@ export const appState = proxy({
localStorage.setItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT, value.toString())
}
},
showEnableBranchingModal: false,
setShowEnableBranchingModal: (value: boolean) => {
appState.showEnableBranchingModal = value
},
showFeaturePreviewModal: false,
setShowFeaturePreviewModal: (value: boolean) => {
appState.showFeaturePreviewModal = value
},
})
export const getAppStateSnapshot = () => snapshot(appState)
-14
View File
@@ -1,14 +0,0 @@
import { proxy, snapshot, useSnapshot } from 'valtio'
export const appUiState = proxy({
showEnableBranchingModal: false,
setShowEnableBranchingModal: (value: boolean) => {
appUiState.showEnableBranchingModal = value
},
})
export const getAppUiStateSnapshot = () => snapshot(appUiState)
export const useAppUiStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
useSnapshot(appUiState, options)