mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 22:29:11 +03:00
Merge branch 'master' into chore/support-updating-email-and-unlinking-identities
This commit is contained in:
25 files changed
+2022
-1246
No files matched your search
@@ -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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
- [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>
|
||||

|
||||
</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>
|
||||

|
||||
</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>
|
||||

|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3} >
|
||||
|
||||
<StepHikeCompact.Details title="Create a new Grafana Dashboard">
|
||||
Create a new Dashboard
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||

|
||||
</StepHikeCompact.Code>
|
||||
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={4} >
|
||||
<StepHikeCompact.Details title="Search for the Supabase template" />
|
||||
|
||||
<StepHikeCompact.Code>
|
||||

|
||||
</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>
|
||||

|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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']
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+1134
-270
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Generated
+229
-745
File diff suppressed because it is too large.
Load diff
@@ -309,6 +309,7 @@ allow_list = [
|
||||
"supabase-community",
|
||||
"supabase-flutter",
|
||||
"supabase-gdscript",
|
||||
"supabase-grafana",
|
||||
"supabase-go",
|
||||
"supabase-js",
|
||||
"supabase-kt",
|
||||
|
||||
Reference in new issue
Block a user