diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx index 9000583ef7f..63f703d7325 100644 --- a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx @@ -306,7 +306,7 @@ export const DatabaseConnectionString = () => { align="start" buttonProps={{ size: 'small', - className: 'w-full justify-between pr-2.5 [&_svg]:h-4', + className: 'w-full pr-2.5 [&_svg]:h-4', }} className="w-full md:w-auto [&>span]:w-1/2 [&>span]:md:w-auto" onSelectId={handleDatabaseChange} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/ReadReplicaForm/ReadReplicaEligibilityWarnings.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/ReadReplicaForm/ReadReplicaEligibilityWarnings.tsx index 62da219a858..0417a681631 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/ReadReplicaForm/ReadReplicaEligibilityWarnings.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/ReadReplicaForm/ReadReplicaEligibilityWarnings.tsx @@ -1,8 +1,4 @@ import { SupportCategories } from '@supabase/shared-types/out/constants' -import Link from 'next/link' -import { useEffect, useState } from 'react' -import { toast } from 'sonner' - import { useParams } from 'common' import { SupportLink } from 'components/interfaces/Support/SupportLink' import { DocsButton } from 'components/ui/DocsButton' @@ -13,8 +9,12 @@ import { MAX_REPLICAS_ABOVE_XL, MAX_REPLICAS_BELOW_XL } from 'data/read-replicas import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import Link from 'next/link' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' import { Button } from 'ui' import { Admonition } from 'ui-patterns' + import { useCheckEligibilityDeployReplica } from './useCheckEligibilityDeployReplica' export const ReadReplicaEligibilityWarnings = () => { @@ -138,9 +138,7 @@ export const ReadReplicaEligibilityWarnings = () => { addon="computeSize" source="read-replicas" featureProposition="deploy Read Replicas" - > - Change compute size - + /> diff --git a/apps/studio/components/interfaces/Settings/General/DashboardPreferences.tsx b/apps/studio/components/interfaces/Settings/General/DashboardPreferences.tsx new file mode 100644 index 00000000000..050d49db8ab --- /dev/null +++ b/apps/studio/components/interfaces/Settings/General/DashboardPreferences.tsx @@ -0,0 +1,194 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { HelpCircle } from 'lucide-react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { + Button, + Card, + CardContent, + CardFooter, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import ShimmeringLoader, { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import * as z from 'zod' + +import { DatabaseSelector } from '@/components/ui/DatabaseSelector' +import { InlineLink } from '@/components/ui/InlineLink' +import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' +import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' + +const formSchema = z.object({ + defaultDatabase: z.string().optional(), +}) + +export type DashboardPreference = z.infer + +const DEFAULT_PREFERENCE: DashboardPreference = { + defaultDatabase: undefined, +} + +/** + * [Joshen] JFYI am not convinced about the UX of this, will iterate over time + * and only release to public when we're satisfied with how it behaves + * - Where should "Dashboard preferences" live + * - Preferences currently only apply to the user via local storage until we have middleware support that will persist the setting on the project for all users + * - Should selecting which database to run queries for dashboard be an option for users to configure (or for us to just default to) + * - I'd love for this to work seamlessly, but main concern atm is latency which is region dependent + * - Also, current database logic only applies to Table Editor atm, will need to extend it further to other pages + */ + +export const DashboardPreferences = () => { + const { ref: projectRef } = useParams() + + const [dashboardPreferences, setDashboardPreferences, { isLoading }] = + useLocalStorageQuery( + LOCAL_STORAGE_KEYS.DASHBOARD_PREFERENCES(projectRef ?? '_'), + DEFAULT_PREFERENCE + ) + + const { isPending } = useReadReplicasQuery({ projectRef }) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: dashboardPreferences, + values: dashboardPreferences, + mode: 'onSubmit', + reValidateMode: 'onBlur', + }) + + const onSubmit = async (values: DashboardPreference) => { + if (!projectRef) return console.error('Ref is required') + setDashboardPreferences(values) + form.reset(values) + toast.success('Successfully saved dashboard preferences!') + } + + return ( + + + {/* [Joshen] Ideally we're able to persist this for all users in the project, but will need support in our middleware */} + + + {isLoading ? ( + + + + + + ) : ( + +
+ + + ( + + All read queries from the dashboard will run against the selected + database by default + +

+ } + className="[&>div]:md:w-1/2" + > + {isPending ? ( + + ) : ( + + {/* [Joshen] Need to disable unhealthy replicas */} + + field.onChange(id === projectRef ? undefined : id) + } + /> + + )} +
+ )} + /> +
+ + {form.formState.isDirty && ( + + )} + + +
+
+
+ )} +
+
+ ) +} + +const DashboardQueriesDialog = () => { + const { ref } = useParams() + return ( + + + + + + + How does the dashboard interact with your project's database? + + + + + +

+ The dashboard queries your project's database to display data across various interfaces, + such as the Table Editor, the{' '} + Auth Users page, and more. +

+ +

+ You can route these queries to a read replica instead, which will help reduce load on + your primary database. +

+
+ + + + + + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Settings/General/General.tsx b/apps/studio/components/interfaces/Settings/General/General.tsx index 4544eecd26a..03c373f2a3b 100644 --- a/apps/studio/components/interfaces/Settings/General/General.tsx +++ b/apps/studio/components/interfaces/Settings/General/General.tsx @@ -1,28 +1,22 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { BarChart2 } from 'lucide-react' +import { useProjectUpdateMutation } from 'data/projects/project-update-mutation' +import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import Link from 'next/link' import { useForm } from 'react-hook-form' import { toast } from 'sonner' -import * as z from 'zod' - -import { useProjectUpdateMutation } from 'data/projects/project-update-mutation' -import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { + Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, - Alert_Shadcn_, Button, Card, CardContent, CardFooter, + Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - FormMessage_Shadcn_, - Form_Shadcn_, Input_Shadcn_, WarningIcon, } from 'ui' @@ -31,25 +25,17 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageSection, PageSectionContent, - PageSectionDescription, PageSectionMeta, PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' -import PauseProjectButton from './Infrastructure/PauseProjectButton' -import RestartServerButton from './Infrastructure/RestartServerButton' +import * as z from 'zod' export const General = () => { const { data: project } = useSelectedProjectQuery() - const { data: organization } = useSelectedOrganizationQuery() - const isBranch = Boolean(project?.parent_project_ref) - const { projectSettingsRestartProject } = useIsFeatureEnabled([ - 'project_settings:restart_project', - ]) - const { can: canUpdateProject } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', { resource: { project_id: project?.id, @@ -86,184 +72,104 @@ export const General = () => { } return ( - <> - - - - General settings - - - - {isBranch && ( - - - - You are currently on a preview branch of your project - - - Certain settings are not available while you're on a preview branch. To adjust your - project settings, you may return to your{' '} - - main branch - - . - - - )} + + + + General settings + + + + {isBranch && ( + + + + You are currently on a preview branch of your project + + + Certain settings are not available while you're on a preview branch. To adjust your + project settings, you may return to your{' '} + + main branch + + . + + + )} - {project === undefined ? ( - - ) : ( - -
- - - ( - - - - - - - )} - /> - - - - - - - - - - {form.formState.isDirty && ( - - )} - - - -
-
- )} -
-
- - - - - Project availability - - Restart or pause your project when performing maintenance - - - - + {project === undefined ? ( -
-
-

- {projectSettingsRestartProject ? 'Restart project' : 'Restart database'} -

-
-

- Your project will not be available for a few minutes. -

-
-
- -
-
- -
-
-

Pause project

-
-

- Your project will not be accessible while it is paused. -

-
-
- -
+
-
-
- - {!isBranch && ( - - - - Project usage - - - - - -
-
- -
-

Project usage statistics have been moved

-

- You may view your project's usage under your organization's settings -

-
-
- - {!!organization && !!project && ( - )} -
-
-
-
-
- )} - + + + + + + )} +
+
) } diff --git a/apps/studio/components/interfaces/Settings/General/Project.tsx b/apps/studio/components/interfaces/Settings/General/Project.tsx new file mode 100644 index 00000000000..26d40787504 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/General/Project.tsx @@ -0,0 +1,111 @@ +import { BarChart2 } from 'lucide-react' +import Link from 'next/link' +import { Button, Card, CardContent } from 'ui' +import { + PageSection, + PageSectionContent, + PageSectionDescription, + PageSectionMeta, + PageSectionSummary, + PageSectionTitle, +} from 'ui-patterns/PageSection' + +import PauseProjectButton from './Infrastructure/PauseProjectButton' +import RestartServerButton from './Infrastructure/RestartServerButton' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' + +export const Project = () => { + const { data: project } = useSelectedProjectQuery() + const { data: organization } = useSelectedOrganizationQuery() + const isBranch = Boolean(project?.parent_project_ref) + const { projectSettingsRestartProject } = useIsFeatureEnabled([ + 'project_settings:restart_project', + ]) + + return ( + <> + + + + Project availability + + Restart or pause your project when performing maintenance + + + + + + +
+
+

+ {projectSettingsRestartProject ? 'Restart project' : 'Restart database'} +

+
+

+ Your project will not be available for a few minutes. +

+
+
+ +
+
+ +
+
+

Pause project

+
+

+ Your project will not be accessible while it is paused. +

+
+
+ +
+
+
+
+
+ + {!isBranch && ( + + + + Project usage + + + + + +
+
+ +
+

Project usage statistics have been moved

+

+ You may view your project's usage under your organization's settings +

+
+
+ + {!!organization && !!project && ( + + )} +
+
+
+
+
+ )} + + ) +} diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx index 7b37b7926ad..fa68d58cd64 100644 --- a/apps/studio/components/interfaces/Sidebar.tsx +++ b/apps/studio/components/interfaces/Sidebar.tsx @@ -258,9 +258,7 @@ const ProjectLinks = () => { showReports, apiDocsSidePanel: isNewAPIDocsEnabled, }) - const settingsRoutes = generateSettingsRoutes(ref, project, { - platformWebhooks: platformWebhooksEnabled, - }) + const settingsRoutes = generateSettingsRoutes(ref) return ( diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.tsx index a320c023817..37cb8e129bb 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/MobileMenuContent.tsx @@ -99,7 +99,7 @@ export function MobileMenuContent({ }), [ref, project, isUnifiedLogsEnabled, showReports, isNewAPIDocsEnabled] ) - const settingsRoutes = useMemo(() => generateSettingsRoutes(ref, project), [ref, project]) + const settingsRoutes = useMemo(() => generateSettingsRoutes(ref), [ref]) const homeRoute: Route = useMemo( () => ({ diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 9b7262cadf0..0bab5712ebf 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -1,5 +1,5 @@ import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar' -import { generateSettingsMenu } from 'components/layouts/ProjectSettingsLayout/SettingsMenu.utils' +import { useGenerateSettingsMenu } from 'components/layouts/ProjectSettingsLayout/SettingsMenu.utils' import type { Route } from 'components/ui/ui.types' import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor' import type { Project } from 'data/projects/project-detail-query' @@ -182,14 +182,7 @@ export const generateOtherRoutes = ( ] } -export const generateSettingsRoutes = ( - ref?: string, - project?: Project, - features?: { platformWebhooks?: boolean } -): Route[] => { - const settingsMenu = generateSettingsMenu(ref as string, undefined, undefined, { - platformWebhooks: features?.platformWebhooks, - }) +export const generateSettingsRoutes = (ref?: string): Route[] => { return [ { key: 'settings', @@ -198,7 +191,6 @@ export const generateSettingsRoutes = ( link: ref && (IS_PLATFORM ? `/project/${ref}/settings/general` : `/project/${ref}/settings/log-drains`), - items: settingsMenu, disabled: false, }, ] diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx index 03b8c1ccce3..50ddecf86c9 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx @@ -1,15 +1,10 @@ -import { useParams } from 'common' -import { useIsPlatformWebhooksEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { ProductMenu } from 'components/ui/ProductMenu' -import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { withAuth } from 'hooks/misc/withAuth' import { useRouter } from 'next/router' import type { PropsWithChildren } from 'react' import { ProjectLayout } from '../ProjectLayout' -import { generateSettingsMenu } from './SettingsMenu.utils' +import { useGenerateSettingsMenu } from './SettingsMenu.utils' /** * Menu-only component for the settings section. Used by the desktop sidebar and by the @@ -18,48 +13,14 @@ import { generateSettingsMenu } from './SettingsMenu.utils' */ export const SettingsProductMenu = () => { const router = useRouter() - const { ref } = useParams() - const { data: project } = useSelectedProjectQuery() - const { data: organization } = useSelectedOrganizationQuery() - const platformWebhooksEnabled = useIsPlatformWebhooksEnabled() const page = router.pathname.includes('billing') ? router.pathname.split('/')[5] : router.pathname.split('/')[4] - const { - projectAuthAll: authEnabled, - authenticationShowProviders: authProvidersEnabled, - projectEdgeFunctionAll: edgeFunctionsEnabled, - projectStorageAll: storageEnabled, - billingInvoices: invoicesEnabled, - projectSettingsLegacyJwtKeys: legacyJWTKeysEnabled, - projectSettingsLogDrains, - billingAll, - } = useIsFeatureEnabled([ - 'project_auth:all', - 'authentication:show_providers', - 'project_edge_function:all', - 'project_storage:all', - 'billing:invoices', - 'project_settings:legacy_jwt_keys', - 'project_settings:log_drains', - 'billing:all', - ]) + const menu = useGenerateSettingsMenu() - const menuRoutes = generateSettingsMenu(ref, project, organization, { - auth: authEnabled, - authProviders: authProvidersEnabled, - edgeFunctions: edgeFunctionsEnabled, - storage: storageEnabled, - invoices: invoicesEnabled, - legacyJwtKeys: legacyJWTKeysEnabled, - logDrains: projectSettingsLogDrains, - billing: billingAll, - platformWebhooks: platformWebhooksEnabled, - }) - - return + return } interface SettingsLayoutProps { diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx index 7ed196c96dc..001e5ff6012 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.test.tsx @@ -1,5 +1,10 @@ +import { renderHook } from '@testing-library/react' +import { useFlag } from 'common' import { describe, expect, it, vi } from 'vitest' +import { useGenerateSettingsMenu } from './SettingsMenu.utils' +import { useIsPlatformWebhooksEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' + vi.mock('lib/constants', async () => { const actual = await vi.importActual>('lib/constants') return { @@ -8,18 +13,35 @@ vi.mock('lib/constants', async () => { } }) -import { generateSettingsMenu } from './SettingsMenu.utils' +vi.mock('common', () => ({ + useFlag: vi.fn().mockReturnValue(false), + useParams: vi.fn().mockReturnValue({ ref: 'project-ref' }), +})) -describe('generateSettingsMenu', () => { - it('includes webhooks in project settings items', () => { - const menu = generateSettingsMenu( - 'project-ref', - { status: 'ACTIVE_HEALTHY' } as any, - { slug: 'my-org' } as any, - { platformWebhooks: true } - ) +vi.mock('@/hooks/misc/useSelectedOrganization', () => ({ + useSelectedOrganizationQuery: vi.fn().mockReturnValue({ data: { slug: 'my-org' } }), +})) - const configurationGroup = menu.find((group) => group.title === 'Configuration') +vi.mock('@/hooks/misc/useSelectedProject', () => ({ + useSelectedProjectQuery: vi.fn().mockReturnValue({ data: { status: 'ACTIVE_HEALTHY' } }), +})) + +vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({ + useIsFeatureEnabled: vi + .fn() + .mockReturnValue({ projectSettingsLegacyJwtKeys: false, billingAll: true }), +})) + +vi.mock('@/components/interfaces/App/FeaturePreview/FeaturePreviewContext', () => ({ + useIsPlatformWebhooksEnabled: vi.fn().mockReturnValue(true), +})) + +describe('useGenerateSettingsMenu', () => { + it('includes webhooks when platformWebhooks feature is enabled', () => { + vi.mocked(useIsPlatformWebhooksEnabled).mockReturnValue(true) + + const { result } = renderHook(() => useGenerateSettingsMenu()) + const configurationGroup = result.current.find((group) => group.title === 'Configuration') const hasWebhooks = configurationGroup?.items.some( (item) => item.name === 'Webhooks' && item.url === '/project/project-ref/settings/webhooks' ) @@ -28,16 +50,35 @@ describe('generateSettingsMenu', () => { }) it('hides webhooks when platformWebhooks feature is disabled', () => { - const menu = generateSettingsMenu( - 'project-ref', - { status: 'ACTIVE_HEALTHY' } as any, - { slug: 'my-org' } as any, - { platformWebhooks: false } - ) + vi.mocked(useIsPlatformWebhooksEnabled).mockReturnValue(false) - const configurationGroup = menu.find((group) => group.title === 'Configuration') + const { result } = renderHook(() => useGenerateSettingsMenu()) + const configurationGroup = result.current.find((group) => group.title === 'Configuration') const hasWebhooks = configurationGroup?.items.some((item) => item.name === 'Webhooks') expect(hasWebhooks).toBe(false) }) + + it('includes dashboard preferences when flag is enabled', () => { + vi.mocked(useFlag).mockReturnValue(true) + + const { result } = renderHook(() => useGenerateSettingsMenu()) + const preferencesGroup = result.current.find((group) => group.title === 'Preferences') + const hasDashboardPreferences = preferencesGroup?.items.some( + (item) => + item.name === 'Dashboard preferences' && + item.url === '/project/project-ref/settings/preferences' + ) + + expect(hasDashboardPreferences).toBe(true) + }) + + it('hides dashboard preferences when flag is disabled', () => { + vi.mocked(useFlag).mockReturnValue(false) + + const { result } = renderHook(() => useGenerateSettingsMenu()) + const preferencesGroup = result.current.find((group) => group.title === 'Preferences') + + expect(preferencesGroup).toBeUndefined() + }) }) diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx index 7b6e2aec480..da1ce946586 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx @@ -1,25 +1,25 @@ -import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types' -import type { Project } from 'data/projects/project-detail-query' +import { useFlag, useParams } from 'common' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { ArrowUpRight } from 'lucide-react' -import type { Organization } from 'types' -export const generateSettingsMenu = ( - ref?: string, - project?: Project, - organization?: Organization, - features?: { - auth?: boolean - authProviders?: boolean - edgeFunctions?: boolean - storage?: boolean - invoices?: boolean - legacyJwtKeys?: boolean - logDrains?: boolean - billing?: boolean - platformWebhooks?: boolean - } -): ProductMenuGroup[] => { +import { useIsPlatformWebhooksEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' + +export const useGenerateSettingsMenu = () => { + const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() + const { data: organization } = useSelectedOrganizationQuery() + + const showDashboardPreferences = useFlag('dashboardPreferences') + const platformWebhooksEnabled = useIsPlatformWebhooksEnabled() + + const { projectSettingsLegacyJwtKeys: legacyJwtKeysEnabled, billingAll: billingEnabled } = + useIsFeatureEnabled(['project_settings:legacy_jwt_keys', 'billing:all']) + + const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY + if (!IS_PLATFORM) { return [ { @@ -36,12 +36,6 @@ export const generateSettingsMenu = ( ] } - const isProjectActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY - - const legacyJwtKeysEnabled = features?.legacyJwtKeys ?? true - const billingEnabled = features?.billing ?? true - const platformWebhooksEnabled = features?.platformWebhooks ?? false - return [ { title: 'Configuration', @@ -118,6 +112,21 @@ export const generateSettingsMenu = ( }, ], }, + ...(IS_PLATFORM && showDashboardPreferences + ? [ + { + title: 'Preferences', + items: [ + { + name: 'Dashboard preferences', + key: 'preferences', + url: `/project/${ref}/settings/preferences`, + items: [], + }, + ], + }, + ] + : []), { title: 'Integrations', items: [ diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx index 23143762b62..bf73976adab 100644 --- a/apps/studio/components/ui/DatabaseSelector.tsx +++ b/apps/studio/components/ui/DatabaseSelector.tsx @@ -37,6 +37,7 @@ interface DatabaseSelectorProps { onSelectId?: (id: string) => void // Optional callback className?: string align?: 'start' | 'end' + isForm?: boolean } export const DatabaseSelector = ({ @@ -47,6 +48,7 @@ export const DatabaseSelector = ({ buttonProps, align = 'end', className, + isForm = false, }: DatabaseSelectorProps) => { const router = useRouter() const { ref: projectRef } = useParams() @@ -73,7 +75,7 @@ export const DatabaseSelector = ({ const newReplicaURL = `/project/${projectRef}/database/replication?type=Read+Replica` useEffect(() => { - if (_selectedDatabaseId) state.setSelectedDatabaseId(_selectedDatabaseId) + if (_selectedDatabaseId && !isForm) state.setSelectedDatabaseId(_selectedDatabaseId) // eslint-disable-next-line react-hooks/exhaustive-deps }, [_selectedDatabaseId]) @@ -81,16 +83,21 @@ export const DatabaseSelector = ({
- - Source - + {!isForm && ( + + Source + + )}