Merge branch 'master' into chore/support-updating-email-and-unlinking-identities

This commit is contained in:
Joshen Lim authored and GitHub committed 2025-03-03 20:08:36 +08:00
commit c17cd454d8
25 files changed
+2022 -1246

No files matched your search

+63 -193
View File
@@ -4,7 +4,7 @@ title: 'Metrics'
description: 'Observability for your Supabase project'
---
In addition to the reports and charts built in to the Supabase dashboard, each project hosted on the Supabase platform comes with a [Prometheus](https://prometheus.io/)-compatible metrics endpoint, which can be used to gather insight into the health and status of your project.
In addition to the reports and charts built in to the Supabase dashboard, each project hosted on the Supabase platform comes with a [Prometheus](https://prometheus.io/)-compatible metrics endpoint, updated every minute, which can be used to gather insight into the health and status of your project.
You can use this endpoint to ingest data into your own monitoring and alerting infrastructure, as long as it is capable of scraping Prometheus-compatible endpoints, in order to set up custom rules beyond those supported by the Supabase dashboard.
@@ -28,13 +28,40 @@ Your project's metrics endpoint is accessible at `https://<project-ref>.supabase
> curl https://<project-ref>.supabase.co/customer/v1/privileged/metrics --user 'service_role:<service-role-jwt>'
```
## Configuring Prometheus
## Supabase Grafana
For self-hosted Prometheus, modify your `prometheus.yaml` file to add a Supabase job, and set the `metrics_path`, `scheme`, `basic_auth` and `targets` parameters. For example:
The pre-configured Supabase Grafana Dashboard is an advanced version of the [Dashboard's Database Reports](https://supabase.com/dashboard/project/_/reports/database). It visualizes over 200 database performance and health metrics.
![Supabase Grafana](/docs/img/guides/platform/supabase-grafana-prometheus.png)
Instructions are included in the README for deploying the repository using docker.
## Using the metrics endpoint in production
To set up monitoring for your project, you will need two things:
1. A datastore - a place to store the metrics coming from your Supabase project over time
2. A dashboard - a place to visualize the state of your Supabase project for a defined period
### Setting up a metrics datastore
One of the more well-known options is [Prometheus](https://prometheus.io/docs/introduction/overview/) and it is the tool used in this guide.
You can [self-host](https://prometheus.io/docs/prometheus/latest/installation/) Prometheus or choose a managed service to store your metrics. Some of the providers offering managed Prometheus are:
- [Digital Ocean](https://marketplace.digitalocean.com/apps/prometheus)
- [AWS](https://aws.amazon.com/prometheus/)
- [Grafana Cloud](https://grafana.com/products/cloud/metrics/)
Follow the guides for the deployment option you choose
#### Adding a scrape job to Prometheus
For Prometheus, modify your `prometheus.yaml` file to add a Supabase job, and set the `metrics_path`, `scheme`, `basic_auth` and `targets` parameters. For example:
```yaml
scrape_configs:
- job_name: "MyJob"
- job_name: "MySupabaseJob"
metrics_path: "/customer/v1/privileged/metrics"
scheme: https
basic_auth:
@@ -48,198 +75,41 @@ scrape_configs:
group: "MyGroupLabel"
```
Additionally, we [maintain a repo](https://github.com/supabase/supabase-grafana) for a scraping agent to work with Grafana Cloud.
### Setting up a dashboard
## Deploying Supabase Grafana
For this guide, we will be using [Grafana](https://grafana.com/docs/grafana/latest/introduction/).
The pre-configured Supabase Grafana Dashboard is an advanced version of the [Dashboard's Database Reports](https://supabase.com/dashboard/project/_/reports/database). It visualizes over 200 database performance and health metrics and is updated every minute.
You can [self-host](https://grafana.com/docs/grafana/latest/setup-grafana/installation/) Grafana or many providers offer managed Grafana, some of which are listed below:
![Supabase Grafana](/docs/img/guides/platform/supabase-grafana-prometheus.png)
- [DigitalOcean](https://marketplace.digitalocean.com/apps/grafana)
- [AWS](https://aws.amazon.com/grafana/)
- [Grafana Cloud](https://grafana.com/grafana/)
### Deploying locally
Follow the guides of the provider you choose to get Grafana up and running.
<Admonition type="note">
This method uses docker to deploy locally but the same steps can be used to deploy to a dedicated
server, such as [Fly](https://fly.io), [Railway](https://railway.app) or [Digital
Ocean](https://digitalocean.com)
### Adding a data source to Grafana
In the left-hand menu, select `Data sources` and click `Add new data source`.
Select `Prometheus` and enter the connection details for the Prometheus instance you have set up.
Under **Interval behavior**, set the **scraping interval** to 60s and test the data source. Once it has passed, save it.
### Adding the Supabase dashboard
In the left-hand menu, select `Dashboards` and click `New`. From the drop-down, select `Import`.
Copy the raw file from our [supabase-grafana](https://raw.githubusercontent.com/supabase/supabase-grafana/refs/heads/main/grafana/dashboard.json) repository and paste it (or upload the file).
Click `Load` and the dashboard will load from the project specified in your Prometheus job.
### Monitoring your project
You can configure alerts from Prometheus or Grafana. The `supabase-grafana` repository has a selection of [example alerts](https://github.com/supabase/supabase-grafana/blob/main/docs/example-alerts.md) that can be configured.
<Admonition type="caution">
Grafana Cloud has an unofficial integration for scraping Supabase metrics. See their
[docs](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/integrations/integration-reference/integration-supabase/)
for instructions on how to configure it but note that it is not full-featured nor is it supported
by Supabase.
</Admonition>
Prerequisites:
- Must have [Docker](https://www.docker.com/products/docker-desktop/) or a Docker compatible runtime on your machine
<Admonition type="note">
Supabase Grafana only tracks metrics while it's active. For monitoring historical trends, it's better to use a dedicated server that can record data continuously.
</Admonition>
<StepHikeCompact>
<StepHikeCompact.Step step={1} >
<StepHikeCompact.Details title="Download the project">
Download the code from the [GitHub repo](https://github.com/supabase/supabase-grafana/tree/main)
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sh
git clone https://github.com/supabase/supabase-grafana.git
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2} >
<StepHikeCompact.Details title="Create a .env file">
copy the `example.env` into a new `.env`
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sh
cp .env.example .env
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Add your credentials">
- To monitor a single project, add your service_role key from the [API Settings](https://supabase.com/dashboard/project/_/settings/api) and Project ID from the [General Settings](https://supabase.com/dashboard/project/_/settings/general).
- To monitor multiple projects, create an [account access token](https://supabase.com/dashboard/account/tokens) and add it to your `.env` file.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="single-project"
>
<TabPanel id="single-project" label="Monitor a single project">
- Add the values to your `.env` file:
```text .env
SUPABASE_PROJECT_REF="your_project_reference"
SUPABASE_SERVICE_ROLE_KEY="your_service_role"
```
</TabPanel>
<TabPanel id="monitor-an-org" label="monitor multiple projects">
```text .env
SUPABASE_ACCESS_TOKEN="your_access_token"
```
</TabPanel>
</Tabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Add a password to your .env">
<Admonition type="note" label="password manager">
For security, consider using a [password generator](https://bitwarden.com/password-generator/)
</Admonition>
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```text .env
GRAFANA_PASSWORD="your password"
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Deploy Grafana with Docker">
Deploy the server. Once it is active, you can go to <a href='http://localhost:8000/login'>localhost:8000/login</a> in the browser
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```text
docker compose up
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Details title="Login" >
Login with your username (default is `admin`) and the password you created in step 4.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
![Grafana login](/docs/img/guides/platform/grafana-login.png)
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
### Deploying to Grafana cloud
<Admonition type="note">
This section describes the Grafana cloud Supabase template, which is created and maintained by
Grafana.
</Admonition>
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Create a free Grafana Cloud account" >
Go to Grafana\'s [registration form](https://grafana.com/auth/sign-up/create-user?pg=login) and create an account
</StepHikeCompact.Details>
<StepHikeCompact.Code>
![Grafana Registration Page](/docs/img/guides/platform/grafana-registration.png)
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2} >
<StepHikeCompact.Details title="Give a name to your Grafana project">
Grafana will prompt you to create a stack. Give it a name and continue.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
![Create Grafana Stack](/docs/img/guides/platform/create-grafana-stack.png)
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3} >
<StepHikeCompact.Details title="Create a new Grafana Dashboard">
Create a new Dashboard
</StepHikeCompact.Details>
<StepHikeCompact.Code>
![Select Grafana Dashboard](/docs/img/guides/platform/select-grafana-dashboard.png)
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4} >
<StepHikeCompact.Details title="Search for the Supabase template" />
<StepHikeCompact.Code>
![Select Grafana Dashboard](/docs/img/guides/platform/supabase-grafana-template.png)
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5} >
<StepHikeCompact.Details title="Add your credentials and deploy">
Add your service_role key from the [API Settings](https://supabase.com/dashboard/project/_/settings/api) and Project ID from the [General Settings](https://supabase.com/dashboard/project/_/settings/general) to your Grafana credentials. Then install your dashboard.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
![Add credentials](/docs/img/guides/platform/add-credentials-grafana.png)
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
+2 -4
View File
@@ -6,11 +6,11 @@ import { memo, useEffect, type PropsWithChildren, type ReactNode } from 'react'
import { cn } from 'ui'
import { type NavMenuSection } from '~/components/Navigation/Navigation.types'
import DefaultNavigationMenu, {
MenuId,
} from '~/components/Navigation/NavigationMenu/NavigationMenu'
import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils'
import { type NavMenuSection } from '~/components/Navigation/Navigation.types'
import TopNavBar from '~/components/Navigation/NavigationMenu/TopNavBar'
import { DOCS_CONTENT_CONTAINER_ID } from '~/features/ui/helpers.constants'
import { menuState, useMenuMobileOpen } from '~/hooks/useMenuState'
@@ -381,8 +381,6 @@ function SidebarSkeleton({
const mobileMenuOpen = useMenuMobileOpen()
console.log('menuName', menuName, menuId)
return (
<div className={cn('flex flex-row h-full relative', className)}>
{!hideSideNav && (
@@ -425,4 +423,4 @@ function SidebarSkeleton({
)
}
export { TopNavSkeleton, SidebarSkeleton }
export { SidebarSkeleton, TopNavSkeleton }
+3 -3
View File
@@ -38,9 +38,9 @@
"@mdx-js/react": "^2.3.0",
"@next/bundle-analyzer": "^14.2.3",
"@next/mdx": "^14.2.3",
"@octokit/auth-app": "^6.0.3",
"@octokit/core": "^5.1.0",
"@octokit/graphql": "^7.0.2",
"@octokit/auth-app": "^7.0.0",
"@octokit/core": "^6.0.0",
"@octokit/graphql": "^8.0.0",
"@octokit/plugin-paginate-graphql": "^4.0.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-collapsible": "^1.0.3",
@@ -0,0 +1,348 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useEffect } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'
import { useParams } from 'common'
import { ScaffoldSection } from 'components/layouts/Scaffold'
import { convertFromBytes } from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils'
import AlertError from 'components/ui/AlertError'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
import { useRealtimeConfigurationUpdateMutation } from 'data/realtime/realtime-config-mutation'
import {
REALTIME_DEFAULT_CONFIG,
useRealtimeConfigurationQuery,
} from 'data/realtime/realtime-config-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import {
Button,
Card,
CardContent,
CardFooter,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
FormMessage_Shadcn_,
Input_Shadcn_,
Switch,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
const formId = 'realtime-configuration-form'
const FormSchema = z.object({
private_only: z.boolean(),
connection_pool: z.coerce.number().min(1).max(100),
max_concurrent_users: z.coerce.number().min(1).max(50000),
max_events_per_second: z.coerce.number().min(1).max(50000),
max_bytes_per_second: z.coerce.number().min(1).max(10000000),
max_channels_per_client: z.coerce.number().min(1).max(10000),
max_joins_per_second: z.coerce.number().min(1).max(5000),
})
export const RealtimeSettings = () => {
const { ref: projectRef } = useParams()
const canUpdateConfig = useCheckPermissions(PermissionAction.REALTIME_ADMIN_READ, '*')
const { data, error, isLoading, isSuccess, isError } = useRealtimeConfigurationQuery({
projectRef,
})
const { mutate: updateRealtimeConfig, isLoading: isUpdatingConfig } =
useRealtimeConfigurationUpdateMutation({
onSuccess: () => {
form.reset(form.getValues())
toast.success('Successfully updated realtime settings')
},
})
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: REALTIME_DEFAULT_CONFIG,
})
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = (data) => {
if (!projectRef) return console.error('Project ref is required')
updateRealtimeConfig({ ref: projectRef, ...data })
}
useEffect(() => {
// [Joshen] Temp typed with any - API typing marks all the properties as nullable,
// but checked with Filipe that they're not supposed to
if (data) form.reset(data as any)
}, [isSuccess])
return (
<ScaffoldSection isFullWidth>
<Form_Shadcn_ {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
{isError ? (
<AlertError error={error} subject="Failed to retrieve realtime settings" />
) : (
<Card>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="private_only"
render={({ field }) => (
<FormSection
className="!p-0 !pt-2"
header={<FormSectionLabel>Channel restrictions</FormSectionLabel>}
>
<FormSectionContent loading={isLoading} className="!gap-y-2">
<FormItemLayout
layout="flex"
label="Private channels only"
description="If this is enabled, only private channels will be allowed"
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
</FormSectionContent>
</FormSection>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="connection_pool"
render={({ field }) => (
<FormSection
className="!p-0 !py-2"
header={
<FormSectionLabel
description={
<p className="text-foreground-lighter text-sm !mt-1">
Sets connection pool size for Realtime Authorization
</p>
}
>
Connection pool size
</FormSectionLabel>
}
>
<FormSectionContent loading={isLoading} className="!gap-y-2">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
type="number"
disabled={!canUpdateConfig}
value={field.value || ''}
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</FormSectionContent>
</FormSection>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="max_concurrent_users"
render={({ field }) => (
<FormSection
className="!p-0 !py-2"
header={
<FormSectionLabel
description={
<p className="text-foreground-lighter text-sm !mt-1">
Sets maximum number of concurrent users rate limit
</p>
}
>
Max concurrent users
</FormSectionLabel>
}
>
<FormSectionContent loading={isLoading} className="!gap-y-2">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
type="number"
disabled={!canUpdateConfig}
value={field.value || ''}
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</FormSectionContent>
</FormSection>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="max_events_per_second"
render={({ field }) => (
<FormSection
className="!p-0 !py-2"
header={
<FormSectionLabel
description={
<p className="text-foreground-lighter text-sm !mt-1">
Sets maximum number of events per second rate per channel limit
</p>
}
>
Max events per second
</FormSectionLabel>
}
>
<FormSectionContent loading={isLoading} className="!gap-y-2">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
type="number"
disabled={!canUpdateConfig}
value={field.value || ''}
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</FormSectionContent>
</FormSection>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="max_bytes_per_second"
render={({ field }) => {
const { value, unit } = convertFromBytes(field.value ?? 0)
return (
<FormSection
className="!p-0 !py-2"
header={
<FormSectionLabel
description={
<p className="text-foreground-lighter text-sm !mt-1">
Sets maximum number of bytes per second rate per channel limit
</p>
}
>
Max bytes per second
</FormSectionLabel>
}
>
<FormSectionContent loading={isLoading} className="!gap-y-2">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
type="number"
disabled={!canUpdateConfig}
value={field.value || ''}
{...form.register('max_bytes_per_second', { valueAsNumber: true })}
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
{!!field.value ? (
<span className="text-sm text-foreground-lighter">
This is equivalent to {value.toFixed(2)} {unit}
</span>
) : null}
</FormSectionContent>
</FormSection>
)
}}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="max_channels_per_client"
render={({ field }) => (
<FormSection
className="!p-0 !py-2"
header={
<FormSectionLabel
description={
<p className="text-foreground-lighter text-sm !mt-1">
Sets maximum number of channels per client rate limit
</p>
}
>
Max channels per client
</FormSectionLabel>
}
>
<FormSectionContent loading={isLoading} className="!gap-y-2">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
type="number"
disabled={!canUpdateConfig}
value={field.value || ''}
{...form.register('max_channels_per_client', { valueAsNumber: true })}
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</FormSectionContent>
</FormSection>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="max_joins_per_second"
render={({ field }) => (
<FormSection
className="!p-0 !py-2"
header={
<FormSectionLabel
description={
<p className="text-foreground-lighter text-sm !mt-1">
Sets maximum number of joins per second rate limit
</p>
}
>
Max joins per second
</FormSectionLabel>
}
>
<FormSectionContent loading={isLoading} className="!gap-y-2">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
type="number"
disabled={!canUpdateConfig}
value={field.value || ''}
{...form.register('max_joins_per_second', { valueAsNumber: true })}
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</FormSectionContent>
</FormSection>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset(data as any)}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={!canUpdateConfig || isUpdatingConfig || !form.formState.isDirty}
loading={isUpdatingConfig}
>
Save changes
</Button>
</CardFooter>
</Card>
)}
</form>
</Form_Shadcn_>
</ScaffoldSection>
)
}
@@ -14,6 +14,7 @@ export interface RealtimeLayoutProps {
const RealtimeLayout = ({ title, children }: PropsWithChildren<RealtimeLayoutProps>) => {
const project = useSelectedProject()
const enableRealtimeSettings = useFlag('enableRealtimeSettings')
const router = useRouter()
const page = router.pathname.split('/')[4]
@@ -22,7 +23,12 @@ const RealtimeLayout = ({ title, children }: PropsWithChildren<RealtimeLayoutPro
<ProjectLayout
title={title}
product="Realtime"
productMenu={<ProductMenu page={page} menu={generateRealtimeMenu(project!)} />}
productMenu={
<ProductMenu
page={page}
menu={generateRealtimeMenu(project!, { enableRealtimeSettings })}
/>
}
>
{children}
</ProjectLayout>
@@ -1,8 +1,13 @@
import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import type { Project } from 'data/projects/project-detail-query'
import { IS_PLATFORM } from 'lib/constants'
export const generateRealtimeMenu = (project: Project): ProductMenuGroup[] => {
export const generateRealtimeMenu = (
project: Project,
flags?: { enableRealtimeSettings: boolean }
): ProductMenuGroup[] => {
const ref = project?.ref ?? 'default'
const { enableRealtimeSettings } = flags || {}
return [
{
@@ -25,6 +30,16 @@ export const generateRealtimeMenu = (project: Project): ProductMenuGroup[] => {
url: `/project/${ref}/realtime/policies`,
items: [],
},
...(IS_PLATFORM && enableRealtimeSettings
? [
{
name: 'Settings',
key: 'settings',
url: `/project/${ref}/realtime/settings`,
items: [],
},
]
: []),
],
},
]
@@ -11,6 +11,7 @@ interface Props {
disabled?: boolean
}
/** @deprecated Use Card instead, refer to BasicAuthSettingsForm.tsx for reference */
const FormPanel = ({ children, header, footer }: Props) => (
<FormPanelContainer>
{header && <FormPanelHeader>{header}</FormPanelHeader>}
@@ -66,4 +67,4 @@ const FormPanelFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement
FormPanelFooter.displayName = FormPanelFooter.displayName
export { FormPanel, FormPanelContainer, FormPanelContent, FormPanelHeader, FormPanelFooter }
export { FormPanel, FormPanelContainer, FormPanelContent, FormPanelFooter, FormPanelHeader }
+1 -1
View File
@@ -7,7 +7,7 @@ import { PROJECT_STATUS } from 'lib/constants'
import { ResponseError } from 'types'
import { lintKeys } from './keys'
export type ProjectLintsVariables = {
type ProjectLintsVariables = {
projectRef?: string
}
type ProjectLintResponse = components['schemas']['GetProjectLintsResponseDto']
+4
View File
@@ -0,0 +1,4 @@
export const realtimeKeys = {
configuration: (projectRef: string | undefined) =>
['projects', projectRef, 'realtime', 'configuration'] as const,
}
@@ -0,0 +1,77 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import type { components } from 'data/api'
import { handleError, patch } from 'data/fetchers'
import type { ResponseError } from 'types'
import { realtimeKeys } from './keys'
export type RealtimeConfigurationUpdateVariables = {
ref: string
} & components['schemas']['UpdateRealtimeConfigBodyDto']
export async function updateRealtimeConfiguration({
ref,
private_only,
connection_pool,
max_concurrent_users,
max_events_per_second,
max_bytes_per_second,
max_channels_per_client,
max_joins_per_second,
}: RealtimeConfigurationUpdateVariables) {
if (!ref) return console.error('Project ref is required')
const { data, error } = await patch('/platform/projects/{ref}/config/realtime', {
params: { path: { ref } },
body: {
private_only,
connection_pool,
max_concurrent_users,
max_events_per_second,
max_bytes_per_second,
max_channels_per_client,
max_joins_per_second,
},
})
if (error) handleError(error)
return data
}
type RealtimeConfigurationUpdateData = Awaited<ReturnType<typeof updateRealtimeConfiguration>>
export const useRealtimeConfigurationUpdateMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<
RealtimeConfigurationUpdateData,
ResponseError,
RealtimeConfigurationUpdateVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<
RealtimeConfigurationUpdateData,
ResponseError,
RealtimeConfigurationUpdateVariables
>((vars) => updateRealtimeConfiguration(vars), {
async onSuccess(data, variables, context) {
const { ref } = variables
await queryClient.invalidateQueries(realtimeKeys.configuration(ref))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to update realtime configuration: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}
@@ -0,0 +1,59 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get, handleError } from 'data/fetchers'
import { IS_PLATFORM } from 'lib/constants'
import type { ResponseError } from 'types'
import { realtimeKeys } from './keys'
export type RealtimeConfigurationVariables = {
projectRef?: string
}
export const REALTIME_DEFAULT_CONFIG = {
private_only: false,
connection_pool: 2,
max_concurrent_users: 200,
max_events_per_second: 100,
max_bytes_per_second: 100000,
max_channels_per_client: 100,
max_joins_per_second: 100,
}
export async function getRealtimeConfiguration(
{ projectRef }: RealtimeConfigurationVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('Project ref is required')
const { data, error } = await get(`/platform/projects/{ref}/config/realtime`, {
params: { path: { ref: projectRef } },
signal,
})
if (error) {
if ((error as ResponseError).message === 'Custom realtime config for a project not found') {
return REALTIME_DEFAULT_CONFIG
} else {
handleError(error)
}
}
return data
}
export type RealtimeConfigurationData = Awaited<ReturnType<typeof getRealtimeConfiguration>>
export type RealtimeConfigurationError = ResponseError
export const useRealtimeConfigurationQuery = <TData = RealtimeConfigurationData>(
{ projectRef }: RealtimeConfigurationVariables,
{
enabled = true,
...options
}: UseQueryOptions<RealtimeConfigurationData, RealtimeConfigurationError, TData> = {}
) =>
useQuery<RealtimeConfigurationData, RealtimeConfigurationError, TData>(
realtimeKeys.configuration(projectRef),
({ signal }) => getRealtimeConfiguration({ projectRef }, signal),
{
enabled: enabled && IS_PLATFORM && typeof projectRef !== 'undefined',
...options,
}
)
+1 -1
View File
@@ -40,7 +40,7 @@
"@supabase/auth-js": "catalog:",
"@supabase/pg-meta": "workspace:*",
"@supabase/realtime-js": "catalog:",
"@supabase/shared-types": "0.1.74",
"@supabase/shared-types": "0.1.80",
"@supabase/sql-to-rest": "^0.1.6",
"@supabase/supabase-js": "catalog:",
"@tanstack/react-query": "4.35.7",
@@ -0,0 +1,27 @@
import type { NextPageWithLayout } from 'types'
import { RealtimeSettings } from 'components/interfaces/Realtime/RealtimeSettings'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import RealtimeLayout from 'components/layouts/RealtimeLayout/RealtimeLayout'
import { ScaffoldContainer } from 'components/layouts/Scaffold'
const RealtimePoliciesPage: NextPageWithLayout = () => {
return (
<ScaffoldContainer>
<RealtimeSettings />
</ScaffoldContainer>
)
}
RealtimePoliciesPage.getLayout = (page) => (
<DefaultLayout>
<RealtimeLayout title="Realtime Settings">
<PageLayout title="Realtime Settings" subtitle="Configure your project's Realtime settings">
{page}
</PageLayout>
</RealtimeLayout>
</DefaultLayout>
)
export default RealtimePoliciesPage
@@ -2,10 +2,9 @@ import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { data as DevelopersData } from 'data/Developers'
import {
jobsCount,
latestBlogPosts,
} from '~/.contentlayer/generated/staticContent/_index.json' with { type: 'json' }
import staticContent from '~/.contentlayer/generated/staticContent/_index.json' with { type: 'json' }
const { jobsCount, latestBlogPosts } = staticContent
type LinkProps = {
text: string
+3 -2
View File
@@ -1,8 +1,9 @@
import React from 'react'
import { Button } from 'ui'
import { githubStars } from '~/.contentlayer/generated/staticContent/_index.json' with { type: 'json' }
import staticContent from '~/.contentlayer/generated/staticContent/_index.json' with { type: 'json' }
import { useSendTelemetryEvent } from '~/lib/telemetry'
const { githubStars } = staticContent
const GitHubButton = () => {
const kFormatter = (num: number) => {
const kFormat = Math.floor(num / 1000)
+8 -6
View File
@@ -1,22 +1,24 @@
import React, { Dispatch, SetStateAction } from 'react'
import { AnimatePresence, LazyMotion, domAnimation, m } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import { m, AnimatePresence, LazyMotion, domAnimation } from 'framer-motion'
import { Dispatch, SetStateAction } from 'react'
import { DEFAULT_EASE } from '~/lib/animations'
import { Accordion, Button, TextLink } from 'ui'
import { DEFAULT_EASE } from '~/lib/animations'
import MenuItem from './MenuItem'
import { useIsLoggedIn, useIsUserLoading } from 'common'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
import { useKey } from 'react-use'
import { useIsLoggedIn, useIsUserLoading } from 'common'
import { ChevronRight } from 'lucide-react'
import { useKey } from 'react-use'
import staticContent from '~/.contentlayer/generated/staticContent/_index.json' with { type: 'json' }
import ProductModulesData from '~/data/ProductModules'
import { jobsCount } from '~/.contentlayer/generated/staticContent/_index.json' with { type: 'json' }
import { useSendTelemetryEvent } from '~/lib/telemetry'
const { jobsCount } = staticContent
interface Props {
open: boolean
setOpen: Dispatch<SetStateAction<boolean>>
+3 -1
View File
@@ -10,7 +10,7 @@ import prettier from 'prettier'
async function generate() {
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
const pages = await globby([
const unsortedPages = await globby([
'pages/*.js',
'pages/*.tsx',
'pages/*.mdx',
@@ -31,6 +31,8 @@ async function generate() {
'.next/server/pages/features/*.html',
])
const pages = unsortedPages.sort((a, b) => a.localeCompare(b))
const blogUrl = 'blog'
const caseStudiesUrl = 'case-studies'
const customerStoriesUrl = 'customers'
+24 -5
View File
@@ -45,7 +45,20 @@ const nextConfig = {
assetPrefix: getAssetPrefix(),
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
trailingSlash: false,
transpilePackages: ['ui', 'ui-patterns', 'common', 'shared-data', 'icons', 'api-types'],
transpilePackages: [
'ui',
'ui-patterns',
'common',
'shared-data',
'icons',
'api-types',
// needed to make the octokit packages work in /changelog
'@octokit/plugin-paginate-graphql',
],
experimental: {
// needed to make the octokit packages work in /changelog
esmExternals: 'loose',
},
reactStrictMode: true,
swcMinify: true,
images: {
@@ -81,10 +94,16 @@ const nextConfig = {
headers: [{ key: 'cache-control', value: 'public, max-age=86400' }],
},
{
source: "/(.*)",
headers: [{
key: 'Strict-Transport-Security',
value: process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && process.env.VERCEL === '1' ? 'max-age=1200; includeSubDomains' : '' }],
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value:
process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && process.env.VERCEL === '1'
? 'max-age=1200; includeSubDomains'
: '',
},
],
},
]
},
+4 -4
View File
@@ -22,11 +22,11 @@
"@mdx-js/react": "^2.3.0",
"@next/bundle-analyzer": "^14.2.3",
"@next/mdx": "^14.2.3",
"@octokit/auth-app": "^6.0.3",
"@octokit/core": "^5.0.2",
"@octokit/graphql": "^7.0.2",
"@octokit/auth-app": "^7.0.0",
"@octokit/core": "^6.0.0",
"@octokit/graphql": "^8.0.0",
"@octokit/plugin-paginate-graphql": "^4.0.0",
"@octokit/rest": "^20.0.2",
"@octokit/rest": "^21.0.0",
"@radix-ui/react-dialog": "^1.0.5",
"@supabase/supabase-js": "catalog:",
"@vercel/og": "^0.6.2",
+2 -2
View File
@@ -1,5 +1,6 @@
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/outline'
import { createAppAuth } from '@octokit/auth-app'
import { Octokit } from '@octokit/core'
import { paginateGraphql } from '@octokit/plugin-paginate-graphql'
import { Octokit as OctokitRest } from '@octokit/rest'
import dayjs from 'dayjs'
@@ -80,7 +81,7 @@ export const getServerSideProps: GetServerSideProps = async ({ res, query }) =>
// Process as of Feb. 2024:
// create a Release each month and create a corresponding changelog discussion
// — we don't want to pull in both the changelog entry and the release entry
// — we don't want to pull in both the changelog entry and the release entry
// — we want to ignore new releases and only show the old ones that don't have a corresponding changelog discussion
// — so we have this list of old releases that we want to show
const oldReleases = [
@@ -94,7 +95,6 @@ export const getServerSideProps: GetServerSideProps = async ({ res, query }) =>
// uses the graphql api
async function fetchDiscussions(owner: string, repo: string, categoryId: string, cursor: string) {
const { Octokit } = await import('@octokit/core')
const ExtendedOctokit = Octokit.plugin(paginateGraphql)
type ExtendedOctokit = InstanceType<typeof ExtendedOctokit>
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -22,6 +22,6 @@
"pg": "^8.13.1",
"postgres-array": "^3.0.2",
"typescript": "~5.5.0",
"vitest": "^2.1.8"
"vitest": "^3.0.0"
}
}
-1
View File
@@ -19,7 +19,6 @@
"dependencies": {
"@headlessui/react": "^1.7.17",
"@hookform/resolvers": "^3.1.1",
"@mertasan/tailwindcss-variables": "^2.2.3",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-aspect-ratio": "^1.0.3",
+229 -745
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -309,6 +309,7 @@ allow_list = [
"supabase-community",
"supabase-flutter",
"supabase-gdscript",
"supabase-grafana",
"supabase-go",
"supabase-js",
"supabase-kt",