mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Joshen/fe 2642 project settings allow to select a replica to power (#43626)
## Context Related to dashboard scalability Previous PR: https://github.com/supabase/supabase/pull/42856 Note: Changes are all feature flagged still and I'm still not entirely convinced with the current UX Will iterate as as go along, and only make this publicly available when we're satisfied with the behaviour Adds a "Dashboard preference" section to the project settings <img width="265" height="740" alt="image" src="https://github.com/user-attachments/assets/6ce1aa19-26c2-47c6-a9c4-595137266631" /> In which users can then select which database they'd like to use for read queries run from the dashboard Note: Everything is local storage for now, but we'd need middleware support if we want to make this setting persist for all users on the project <img width="791" height="434" alt="image" src="https://github.com/user-attachments/assets/e651d6d9-fed4-4da4-b552-c9f93f8d46d3" /> Added a dialog as well to further explain what this implies <img width="610" height="312" alt="image" src="https://github.com/user-attachments/assets/0aa957af-cb51-476f-aa79-8948a7cbe5ae" /> ## To test - Choosing a replica in dashboard preferences will only affect the table editor as thats the only place that's set up so far to use a replica for read queries (I'll need to follow up for other parts of the dashboard in subsequent PRs)
This commit is contained in:
1 parent
a753a599ea
commit
0b97e0ff86
18 files changed
+596
-336
No files matched your search
@@ -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}
|
||||
|
||||
+5
-7
@@ -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
|
||||
</UpgradePlanButton>
|
||||
/>
|
||||
<DocsButton href={`${DOCS_URL}/guides/platform/read-replicas#prerequisites`} />
|
||||
</div>
|
||||
</Admonition>
|
||||
|
||||
@@ -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<typeof formSchema>
|
||||
|
||||
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<DashboardPreference>(
|
||||
LOCAL_STORAGE_KEYS.DASHBOARD_PREFERENCES(projectRef ?? '_'),
|
||||
DEFAULT_PREFERENCE
|
||||
)
|
||||
|
||||
const { isPending } = useReadReplicasQuery({ projectRef })
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
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 (
|
||||
<PageSection>
|
||||
<PageSectionContent className="flex flex-col gap-y-4">
|
||||
{/* [Joshen] Ideally we're able to persist this for all users in the project, but will need support in our middleware */}
|
||||
<Admonition
|
||||
type="note"
|
||||
title="Preferences currently do not affect other members of this project"
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<GenericSkeletonLoader />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="defaultDatabase"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Preferred database for dashboard queries"
|
||||
description={
|
||||
<p>
|
||||
All read queries from the dashboard will run against the selected
|
||||
database by default
|
||||
<DashboardQueriesDialog />
|
||||
</p>
|
||||
}
|
||||
className="[&>div]:md:w-1/2"
|
||||
>
|
||||
{isPending ? (
|
||||
<ShimmeringLoader />
|
||||
) : (
|
||||
<FormControl_Shadcn_>
|
||||
{/* [Joshen] Need to disable unhealthy replicas */}
|
||||
<DatabaseSelector
|
||||
isForm
|
||||
buttonProps={{ size: 'small', className: 'w-full' }}
|
||||
selectedDatabaseId={field.value ?? projectRef}
|
||||
onSelectId={(id) =>
|
||||
field.onChange(id === projectRef ? undefined : id)
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
)}
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{form.formState.isDirty && (
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={() => form.reset(dashboardPreferences)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" htmlType="submit" disabled={!form.formState.isDirty}>
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
|
||||
const DashboardQueriesDialog = () => {
|
||||
const { ref } = useParams()
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="ml-1 translate-y-0.5">
|
||||
<HelpCircle size={14} className="hover:text-foreground transition" />
|
||||
</DialogTrigger>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>How does the dashboard interact with your project's database?</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogSectionSeparator />
|
||||
|
||||
<DialogSection className="flex flex-col gap-y-2">
|
||||
<p className="text-sm">
|
||||
The dashboard queries your project's database to display data across various interfaces,
|
||||
such as the <InlineLink href={`/project/${ref}/editor`}>Table Editor</InlineLink>, the{' '}
|
||||
<InlineLink href={`/project/${ref}/auth/users`}>Auth Users</InlineLink> page, and more.
|
||||
</p>
|
||||
|
||||
<p className="text-sm">
|
||||
You can route these queries to a read replica instead, which will help reduce load on
|
||||
your primary database.
|
||||
</p>
|
||||
</DialogSection>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild className="opacity-100">
|
||||
<Button type="default">Understood</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>General settings</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
{isBranch && (
|
||||
<Alert_Shadcn_ variant="default">
|
||||
<WarningIcon />
|
||||
<AlertTitle_Shadcn_>
|
||||
You are currently on a preview branch of your project
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Certain settings are not available while you're on a preview branch. To adjust your
|
||||
project settings, you may return to your{' '}
|
||||
<Link
|
||||
href={`/project/${project?.parent_project_ref}/settings/general`}
|
||||
className="text-brand"
|
||||
>
|
||||
main branch
|
||||
</Link>
|
||||
.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>General settings</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
{isBranch && (
|
||||
<Alert_Shadcn_ variant="default">
|
||||
<WarningIcon />
|
||||
<AlertTitle_Shadcn_>
|
||||
You are currently on a preview branch of your project
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Certain settings are not available while you're on a preview branch. To adjust your
|
||||
project settings, you may return to your{' '}
|
||||
<Link
|
||||
href={`/project/${project?.parent_project_ref}/settings/general`}
|
||||
className="text-brand"
|
||||
>
|
||||
main branch
|
||||
</Link>
|
||||
.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
|
||||
{project === undefined ? (
|
||||
<GenericSkeletonLoader />
|
||||
) : (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Project name"
|
||||
description="Displayed throughout the dashboard."
|
||||
className="[&>div]:md:w-1/2"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
disabled={isBranch || !canUpdateProject}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Project ID"
|
||||
description="Reference used in APIs and URLs."
|
||||
className="[&>div]:md:w-1/2 [&>div>div]:md:w-full"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input copy readOnly size="small" value={project?.ref ?? ''} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{form.formState.isDirty && (
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="button"
|
||||
disabled={isUpdating}
|
||||
onClick={() => form.reset({ name: project?.name ?? '' })}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
!form.formState.isDirty || isUpdating || !canUpdateProject || isBranch
|
||||
}
|
||||
loading={isUpdating}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
<PageSection id="restart-project">
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Project availability</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
Restart or pause your project when performing maintenance
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
{project === undefined ? (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col @lg:flex-row @lg:justify-between @lg:items-center gap-4">
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{projectSettingsRestartProject ? 'Restart project' : 'Restart database'}
|
||||
</p>
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your project will not be available for a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<RestartServerButton />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div
|
||||
className="flex w-full flex-col @lg:flex-row @lg:justify-between @lg:items-center gap-4"
|
||||
id="pause-project"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm">Pause project</p>
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your project will not be accessible while it is paused.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<PauseProjectButton />
|
||||
</div>
|
||||
<GenericSkeletonLoader />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
{!isBranch && (
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Project usage</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col @lg:flex-row @lg:justify-between @lg:items-center gap-4">
|
||||
<div className="flex space-x-4">
|
||||
<BarChart2 strokeWidth={2} />
|
||||
<div>
|
||||
<p className="text-sm">Project usage statistics have been moved</p>
|
||||
<p className="text-foreground-light text-sm">
|
||||
You may view your project's usage under your organization's settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!organization && !!project && (
|
||||
<Button asChild type="default">
|
||||
<Link href={`/org/${organization.slug}/usage?projectRef=${project.ref}`}>
|
||||
View project usage
|
||||
</Link>
|
||||
) : (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Project name"
|
||||
description="Displayed throughout the dashboard."
|
||||
className="[&>div]:md:w-1/2"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
disabled={isBranch || !canUpdateProject}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Project ID"
|
||||
description="Reference used in APIs and URLs."
|
||||
className="[&>div]:md:w-1/2 [&>div>div]:md:w-full"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input copy readOnly size="small" value={project?.ref ?? ''} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{form.formState.isDirty && (
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="button"
|
||||
disabled={isUpdating}
|
||||
onClick={() => form.reset({ name: project?.name ?? '' })}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
)}
|
||||
</>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
!form.formState.isDirty || isUpdating || !canUpdateProject || isBranch
|
||||
}
|
||||
loading={isUpdating}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<PageSection id="restart-project">
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Project availability</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
Restart or pause your project when performing maintenance
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col @lg:flex-row @lg:justify-between @lg:items-center gap-4">
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{projectSettingsRestartProject ? 'Restart project' : 'Restart database'}
|
||||
</p>
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your project will not be available for a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<RestartServerButton />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div
|
||||
className="flex w-full flex-col @lg:flex-row @lg:justify-between @lg:items-center gap-4"
|
||||
id="pause-project"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm">Pause project</p>
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your project will not be accessible while it is paused.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<PauseProjectButton />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
{!isBranch && (
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Project usage</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col @lg:flex-row @lg:justify-between @lg:items-center gap-4">
|
||||
<div className="flex space-x-4">
|
||||
<BarChart2 strokeWidth={2} />
|
||||
<div>
|
||||
<p className="text-sm">Project usage statistics have been moved</p>
|
||||
<p className="text-foreground-light text-sm">
|
||||
You may view your project's usage under your organization's settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!organization && !!project && (
|
||||
<Button asChild type="default">
|
||||
<Link href={`/org/${organization.slug}/usage?projectRef=${project.ref}`}>
|
||||
View project usage
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -258,9 +258,7 @@ const ProjectLinks = () => {
|
||||
showReports,
|
||||
apiDocsSidePanel: isNewAPIDocsEnabled,
|
||||
})
|
||||
const settingsRoutes = generateSettingsRoutes(ref, project, {
|
||||
platformWebhooks: platformWebhooksEnabled,
|
||||
})
|
||||
const settingsRoutes = generateSettingsRoutes(ref)
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
|
||||
+1
-1
@@ -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(
|
||||
() => ({
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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 <ProductMenu page={page} menu={menuRoutes} />
|
||||
return <ProductMenu page={page} menu={menu} />
|
||||
}
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
|
||||
@@ -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<Record<string, unknown>>('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()
|
||||
})
|
||||
})
|
||||
@@ -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: [
|
||||
|
||||
@@ -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 = ({
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<div className={cn('flex cursor-pointer', className)}>
|
||||
<span className="flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
|
||||
Source
|
||||
</span>
|
||||
{!isForm && (
|
||||
<span className="flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
|
||||
Source
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
type="default"
|
||||
icon={isLoading && <Loader2 className="animate-spin" />}
|
||||
iconRight={<ChevronDown strokeWidth={1.5} size={12} />}
|
||||
iconRight={
|
||||
<ChevronDown strokeWidth={1.5} size={12} className="absolute right-2 top-2" />
|
||||
}
|
||||
{...buttonProps}
|
||||
className={cn(
|
||||
'pr-2 rounded-l-none',
|
||||
'pr-2 justify-start',
|
||||
!isForm && 'rounded-l-none',
|
||||
variant === 'connected-on-right' && 'rounded-r-none',
|
||||
variant === 'connected-on-left' && 'rounded-l-none border-l-0',
|
||||
variant === 'connected-on-both' && 'rounded-none border-x-0',
|
||||
@@ -127,12 +134,12 @@ export const DatabaseSelector = ({
|
||||
value={option.id}
|
||||
className="cursor-pointer w-full"
|
||||
onSelect={() => {
|
||||
state.setSelectedDatabaseId(option.id)
|
||||
if (!isForm) state.setSelectedDatabaseId(option.id)
|
||||
setOpen(false)
|
||||
onSelectId(option.id)
|
||||
}}
|
||||
onClick={() => {
|
||||
state.setSelectedDatabaseId(option.id)
|
||||
if (!isForm) state.setSelectedDatabaseId(option.id)
|
||||
setOpen(false)
|
||||
onSelectId(option.id)
|
||||
}}
|
||||
@@ -184,12 +191,12 @@ export const DatabaseSelector = ({
|
||||
value={database.identifier}
|
||||
className="cursor-pointer w-full"
|
||||
onSelect={() => {
|
||||
state.setSelectedDatabaseId(database.identifier)
|
||||
if (!isForm) state.setSelectedDatabaseId(database.identifier)
|
||||
setOpen(false)
|
||||
onSelectId(database.identifier)
|
||||
}}
|
||||
onClick={() => {
|
||||
state.setSelectedDatabaseId(database.identifier)
|
||||
if (!isForm) state.setSelectedDatabaseId(database.identifier)
|
||||
setOpen(false)
|
||||
onSelectId(database.identifier)
|
||||
}}
|
||||
|
||||
@@ -73,7 +73,13 @@ export const UpgradePlanButton = ({
|
||||
: `/project/${ref ?? '_'}/settings/addons?panel=${addon}&source=${source}`
|
||||
: `/org/${slug ?? '_'}/billing?panel=subscriptionPlan&source=${source}`
|
||||
|
||||
const linkChildren = children || (!!addon ? 'Enable add-on' : `Upgrade to ${plan}`)
|
||||
const linkChildren =
|
||||
children ||
|
||||
(isOnPaidPlanAndRequestingToPurchaseAddon
|
||||
? addon === 'computeSize'
|
||||
? 'Change compute size'
|
||||
: 'Enable add-on'
|
||||
: `Upgrade to ${plan}`)
|
||||
const link = billingAll ? (
|
||||
<Link href={href}>{linkChildren}</Link>
|
||||
) : (
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useFeatureFlags, useFlag } from 'common'
|
||||
import { LOCAL_STORAGE_KEYS, useFeatureFlags, useFlag, useParams } from 'common'
|
||||
import type { components } from 'data/api'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError, UseCustomQueryOptions } from 'types'
|
||||
|
||||
import { replicaKeys } from './keys'
|
||||
import { DashboardPreference } from '@/components/interfaces/Settings/General/DashboardPreferences'
|
||||
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
|
||||
export const MAX_REPLICAS_BELOW_XL = 2
|
||||
@@ -69,29 +71,36 @@ export const useConnectionStringForReadOps = (): {
|
||||
identifier: string | undefined
|
||||
connectionString: string | undefined | null
|
||||
} => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { hasLoaded: flagsLoaded } = useFeatureFlags()
|
||||
const defaultToReadReplicaConnectionString = useFlag('defaultToReadReplicaConnectionString')
|
||||
const showDashboardPreferences = useFlag('dashboardPreferences')
|
||||
|
||||
const [dashboardPreferences, , { isLoading: isLoadingLocalStorage }] =
|
||||
useLocalStorageQuery<DashboardPreference>(
|
||||
LOCAL_STORAGE_KEYS.DASHBOARD_PREFERENCES(projectRef ?? '_'),
|
||||
{}
|
||||
)
|
||||
|
||||
const { data: project, isSuccess: isSuccessProject } = useSelectedProjectQuery()
|
||||
const { data: databases = [], isLoading: isLoadingDatabases } = useReadReplicasQuery({
|
||||
projectRef: project?.ref,
|
||||
})
|
||||
|
||||
const readReplicas = databases.filter(
|
||||
(x) => x.identifier !== project?.ref && x.status === 'ACTIVE_HEALTHY'
|
||||
)
|
||||
const readReplica = readReplicas.some((x) => x.region === project?.region)
|
||||
? readReplicas.find((x) => x.region === project?.region)
|
||||
: readReplicas[0]
|
||||
|
||||
if (!isSuccessProject || isLoadingDatabases || !flagsLoaded) {
|
||||
if (!isSuccessProject || isLoadingDatabases || !flagsLoaded || isLoadingLocalStorage) {
|
||||
return { connectionString: undefined, type: undefined, identifier: undefined }
|
||||
}
|
||||
|
||||
if (!defaultToReadReplicaConnectionString) {
|
||||
if (!showDashboardPreferences) {
|
||||
return { type: 'primary', identifier: project.ref, connectionString: project.connectionString }
|
||||
}
|
||||
|
||||
const readReplicas = databases.filter(
|
||||
(x) => x.identifier !== project?.ref && x.status === 'ACTIVE_HEALTHY'
|
||||
)
|
||||
const readReplica = readReplicas.find(
|
||||
(x) => x.identifier === dashboardPreferences.defaultDatabase
|
||||
)
|
||||
|
||||
return {
|
||||
type: !!readReplica ? 'replica' : 'primary',
|
||||
identifier: !!readReplica ? readReplica.identifier : project.ref,
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
PageHeaderTitle,
|
||||
} from 'ui-patterns/PageHeader'
|
||||
|
||||
import { Project } from '@/components/interfaces/Settings/General/Project'
|
||||
|
||||
const ProjectSettings: NextPageWithLayout = () => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
@@ -55,7 +57,7 @@ const ProjectSettings: NextPageWithLayout = () => {
|
||||
</PageHeader>
|
||||
<PageContainer size="small">
|
||||
<General />
|
||||
|
||||
<Project />
|
||||
{/* this is only settable on compliance orgs, currently that means HIPAA orgs */}
|
||||
{!isBranch && hasHipaaAddon && <ComplianceConfig />}
|
||||
{projectSettingsCustomDomains && <CustomDomainConfig />}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const Settings: NextPageWithLayout = () => {
|
||||
return <>{/* <h1>Use this as a page template for settings pages</h1> */}</>
|
||||
}
|
||||
|
||||
Settings.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<SettingsLayout>{page}</SettingsLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default Settings
|
||||
@@ -0,0 +1,40 @@
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { PageContainer } from 'ui-patterns/PageContainer'
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderDescription,
|
||||
PageHeaderMeta,
|
||||
PageHeaderSummary,
|
||||
PageHeaderTitle,
|
||||
} from 'ui-patterns/PageHeader'
|
||||
|
||||
import { DashboardPreferences } from '@/components/interfaces/Settings/General/DashboardPreferences'
|
||||
|
||||
const Preferences: NextPageWithLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader size="small">
|
||||
<PageHeaderMeta>
|
||||
<PageHeaderSummary>
|
||||
<PageHeaderTitle>Dashboard Preferences</PageHeaderTitle>
|
||||
<PageHeaderDescription>
|
||||
Configure how the dashboard interacts with your project
|
||||
</PageHeaderDescription>
|
||||
</PageHeaderSummary>
|
||||
</PageHeaderMeta>
|
||||
</PageHeader>
|
||||
<PageContainer size="small">
|
||||
<DashboardPreferences />
|
||||
</PageContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Preferences.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<SettingsLayout title="General">{page}</SettingsLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
export default Preferences
|
||||
@@ -13,6 +13,7 @@ export const LOCAL_STORAGE_KEYS = {
|
||||
FEEDBACK_WIDGET_SCREENSHOT: 'feedback-widget-screenshot',
|
||||
INCIDENT_BANNER_DISMISSED_IDS: 'incident-banner-dismissed-ids',
|
||||
MAINTENANCE_BANNER_DISMISSED: (id: string) => `maintenance-banner-dismissed-${id}`,
|
||||
DASHBOARD_PREFERENCES: (ref: string) => `dashboard-preferences-${ref}`,
|
||||
|
||||
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
|
||||
UI_PREVIEW_CLS: 'supabase-ui-cls',
|
||||
|
||||
Reference in new issue
Block a user