diff --git a/studio/components/interfaces/Account/Preferences/AccountInformation.tsx b/studio/components/interfaces/Account/Preferences/AccountInformation.tsx new file mode 100644 index 00000000000..467fec10589 --- /dev/null +++ b/studio/components/interfaces/Account/Preferences/AccountInformation.tsx @@ -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 ( + + Account Information + + } + > + +
+ + + {session?.user.app_metadata.provider === 'email' && ( +
+
+

Password

+
+
+ + + + + +
+
+ )} +
+
+
+ ) +} + +export default AccountInformation diff --git a/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx b/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx new file mode 100644 index 00000000000..15f9992a9bc --- /dev/null +++ b/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx @@ -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 ( + Analytics}> + + + + + ) +} + +export default AnalyticsSettings diff --git a/studio/components/interfaces/Account/Preferences/ThemeSettings.tsx b/studio/components/interfaces/Account/Preferences/ThemeSettings.tsx new file mode 100644 index 00000000000..430a4e32d94 --- /dev/null +++ b/studio/components/interfaces/Account/Preferences/ThemeSettings.tsx @@ -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 ( + Theme}> + + : } + onChange={(value: string) => setTheme(value)} + > + + System default + + + Dark + + + Light + + + + + ) +} + +export default ThemeSettings diff --git a/studio/components/interfaces/Account/Preferences/index.ts b/studio/components/interfaces/Account/Preferences/index.ts new file mode 100644 index 00000000000..c31bb56fd8e --- /dev/null +++ b/studio/components/interfaces/Account/Preferences/index.ts @@ -0,0 +1,5 @@ +import AccountInformation from './AccountInformation' +import AnalyticsSettings from './AnalyticsSettings' +import ThemeSettings from './ThemeSettings' + +export { AccountInformation, AnalyticsSettings, ThemeSettings } diff --git a/studio/components/interfaces/App/FeaturePreviewContext.tsx b/studio/components/interfaces/App/FeaturePreviewContext.tsx new file mode 100644 index 00000000000..9963867ad69 --- /dev/null +++ b/studio/components/interfaces/App/FeaturePreviewContext.tsx @@ -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({ + 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 {children} +} + +// 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] +} diff --git a/studio/components/interfaces/App/FeaturePreviewModal.tsx b/studio/components/interfaces/App/FeaturePreviewModal.tsx new file mode 100644 index 00000000000..f46a3172da1 --- /dev/null +++ b/studio/components/interfaces/App/FeaturePreviewModal.tsx @@ -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(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 ( + snap.setShowFeaturePreviewModal(false)} + > +
+
+ + {FEATURE_PREVIEWS.map((feature) => { + const isEnabled = flags[feature.key] ?? false + + return ( +
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 ? ( + + ) : ( + + )} +

{feature.name}

+
+ ) + })} +
+
+
+
+

{selectedFeature?.name}

+ +
+ +
+
+
+ ) +} + +export default FeaturePreviewModal diff --git a/studio/components/interfaces/BranchManagement/BranchManagement.tsx b/studio/components/interfaces/BranchManagement/BranchManagement.tsx index f9c944a0d8c..d1853e4145d 100644 --- a/studio/components/interfaces/BranchManagement/BranchManagement.tsx +++ b/studio/components/interfaces/BranchManagement/BranchManagement.tsx @@ -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() diff --git a/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingButton.tsx b/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingButton.tsx index f37a8adf19e..f3e48ef9a08 100644 --- a/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingButton.tsx +++ b/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingButton.tsx @@ -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 = diff --git a/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx b/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx index 4fae3c885c5..0640ea528ca 100644 --- a/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx +++ b/studio/components/layouts/AppLayout/EnableBranchingButton/EnableBranchingModal.tsx @@ -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() const [addConnectionType, setAddConnectionType] = useState() diff --git a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index c1d8b1aea44..62d15cf9d86 100644 --- a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -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 (
{ -

Account Preferences

+

Account preferences

+ {showFeaturePreviews && ( + snap.setShowFeaturePreviewModal(true)} + onSelect={() => snap.setShowFeaturePreviewModal(true)} + > + +

Feature previews

+
+ )} )} diff --git a/studio/lib/constants/index.ts b/studio/lib/constants/index.ts index 7d5fbe06451..fdecba47324 100644 --- a/studio/lib/constants/index.ts +++ b/studio/lib/constants/index.ts @@ -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 = { diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 3477ed3ad37..e196fd9546a 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -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) { > - {getLayout()} + + {getLayout()} + + diff --git a/studio/pages/account/me.tsx b/studio/pages/account/me.tsx index a19015c0fc8..da46d79dfd0 100644 --- a/studio/pages/account/me.tsx +++ b/studio/pages/account/me.tsx @@ -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 && ( <>
- +
{/* @ts-ignore */} @@ -121,104 +119,3 @@ const ProfileCard = observer(() => { ) }) - -const Profile = ({ profile }: { profile?: ProfileType }) => { - const session = useSession() - - return ( - - Account Information - - } - > - -
- - - {session?.user.app_metadata.provider === 'email' && ( -
-
-

Password

-
- -
- )} -
-
-
- ) -} - -const ThemeSettings = observer(() => { - const { theme, setTheme } = useTheme() - - return ( - Theme}> - - : } - onChange={(value: string) => setTheme(value)} - > - - System default - - - Dark - - - Light - - - - - ) -}) - -const AnalyticsSettings = observer(() => { - const snap = useAppStateSnapshot() - - const onToggleOptIn = () => { - const value = !snap.isOptedInTelemetry ? 'true' : 'false' - snap.setIsOptedInTelemetry(value === 'true') - } - - return ( - Analytics}> - - - - - ) -}) diff --git a/studio/state/app-state.ts b/studio/state/app-state.ts index 8147ee3b8dd..f0c1f75cecb 100644 --- a/studio/state/app-state.ts +++ b/studio/state/app-state.ts @@ -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) diff --git a/studio/state/app.ts b/studio/state/app.ts deleted file mode 100644 index f6f7d9ab9a9..00000000000 --- a/studio/state/app.ts +++ /dev/null @@ -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[1]) => - useSnapshot(appUiState, options)