Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ github_url = "https://github.com/orgs/supabase/discussions/19391"
date_created = "2023-12-03T17:05:20+00:00"
topics = ["realtime", "self-hosting"]
keywords = ["quota", "connections"]
database_id = "01e0dd67-5208-4e65-b489-2afd5a0b2af3"
---

The "Concurrent Peak Connections" quota refers to the maximum number of simultaneous connections to Supabase Realtime.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import AddNewSecretForm from './AddNewSecretForm'
import EdgeFunctionSecret from './EdgeFunctionSecret'
import { EditSecretSheet } from './EditSecretSheet'
import { InlineLink } from '@/components/ui/InlineLink'
import { DOCS_URL } from '@/lib/constants'

export const EdgeFunctionSecrets = () => {
const { ref: projectRef } = useParams()
Expand Down Expand Up @@ -158,8 +160,17 @@ export const EdgeFunctionSecrets = () => {
<TableRow className="[&>td]:hover:bg-inherit">
<TableCell colSpan={headers.length}>
<p className="text-sm text-foreground">No secrets created</p>
<p className="text-sm text-foreground-light">
There are no secrets associated with your project yet
<p className="text-sm text-foreground-lighter">
This project has no custom secrets yet.{' '}
<code className="text-code-inline !text-foreground-lighter whitespace-nowrap">
SUPABASE_*
</code>{' '}
<InlineLink
href={`${DOCS_URL}/guides/functions/secrets#default-secrets`}
>
default secrets
</InlineLink>{' '}
are still available.
</p>
</TableCell>
</TableRow>
Expand Down
50 changes: 6 additions & 44 deletions apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner'
import { InlineLink } from 'components/ui/InlineLink'
import { useFlag } from 'common'

import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { HeaderBanner } from '@/components/interfaces/Organization/HeaderBanner'
import { InlineLink } from '@/components/ui/InlineLink'

const BANNER_DESCRIPTION = (
<>
Expand All @@ -12,54 +10,18 @@ const BANNER_DESCRIPTION = (
)

/**
* Used to display ongoing incidents or maintenances
* Used to display ongoing incidents
*/
export const StatusPageBanner = () => {
const { data: allStatusPageEvents } = useIncidentStatusQuery()
const { incidents = [], maintenanceEvents = [] } = allStatusPageEvents ?? {}

// Only show incident banner for incidents with real impact (not "none")
const highImpactIncident = incidents.find((incident) => incident.impact !== 'none')
const incidentEventId = highImpactIncident?.id ?? ''

const showIncidentBannerOverride =
const showIncidentBanner =
useFlag('ongoingIncident') || process.env.NEXT_PUBLIC_ONGOING_INCIDENT === 'true'

const ongoingMaintenance = maintenanceEvents.length > 0
const maintenanceEventId = maintenanceEvents[0]?.id ?? ''

const [dismissedIncident, setDismissedIncident] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.INCIDENT_BANNER_DISMISSED(incidentEventId),
false
)

const [dismissedMaintenance, setDismissedMaintenance] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.MAINTENANCE_BANNER_DISMISSED(maintenanceEventId),
false
)

if (showIncidentBannerOverride || (highImpactIncident && !dismissedIncident)) {
if (showIncidentBanner) {
return (
<HeaderBanner
variant="warning"
title="We are investigating a technical issue"
description={BANNER_DESCRIPTION}
onDismiss={
showIncidentBannerOverride || !highImpactIncident
? undefined
: () => setDismissedIncident(true)
}
/>
)
}

if (ongoingMaintenance && !dismissedMaintenance) {
return (
<HeaderBanner
variant="note"
title="Scheduled maintenance is in progress"
description={BANNER_DESCRIPTION}
onDismiss={() => setDismissedMaintenance(true)}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,27 +299,25 @@ const EdgeFunctionDetailsLayout = ({
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
{IS_PLATFORM && (
<>
{!!functionSlug && (
<Button
type="default"
icon={<Send />}
onClick={() => {
setIsOpen(true)
sendEvent({
action: 'edge_function_test_side_panel_opened',
groups: {
project: ref ?? 'Unknown',
organization: org?.slug ?? 'Unknown',
},
})
}}
>
Test
</Button>
)}
</>
{!!functionSlug && (
<Button
type="default"
icon={<Send />}
onClick={() => {
setIsOpen(true)
if (IS_PLATFORM) {
sendEvent({
action: 'edge_function_test_side_panel_opened',
groups: {
project: ref ?? 'Unknown',
organization: org?.slug ?? 'Unknown',
},
})
}
}}
>
Test
</Button>
)}
</div>
</PageHeaderAside>
Expand Down
48 changes: 42 additions & 6 deletions apps/studio/lib/api/edgeFunctions.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, describe, it } from 'vitest'
import { describe, expect, it } from 'vitest'

import { isValidEdgeFunctionURL } from './edgeFunctions'

describe('isValidEdgeFunctionURL', () => {
Expand All @@ -9,23 +10,58 @@ describe('isValidEdgeFunctionURL', () => {
'https://projectref.supabase.red/functions/v3/hello-world',
]

const invalidEdgeFunctionUrls = [
const validLocalEdgeFunctionsUrls = [
'https://projectref.notsupabase.com/functions/v1/test',
'https://notsupabase.com/functions/v1/test',
'http://localhost:54321/functions/v1/test-2',
'http://kong:8000/functions/v1/hello-world',
'https://127.0.0.1:54321/functions/v1/test-3',
'https://127.0.0.1:54321/functions/v1/test-5',
]

const invalidPlatformEdgeFunctionUrls = [
'https://notsupabase.com/functions/v1/test',
'https://projectref.notsupabase.com/functions/v1/test',
'https://localhost?https://aaaa.supabase.co/functions/v1/xxx',
'https://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
'http://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
]

it('should match valid edge function URLs', () => {
const invalidEdgeFunctionUrls = [
'https://localhost?https://aaaa.supabase.co/functions/v1/xxx',
'https://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
'http://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
]

it('should match valid edge function URLs on platform', () => {
for (const url of validEdgeFunctionUrls) {
expect(isValidEdgeFunctionURL(url), `Expected ${url} to be valid`).toBe(true)
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be valid`).toBe(true)
}
})

it('should not match local URLs on platform', () => {
for (const url of validLocalEdgeFunctionsUrls) {
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be invalid on platform`).toBe(
false
)
}
})

it('should match valid local edge function URLs off platform', () => {
for (const url of validLocalEdgeFunctionsUrls) {
expect(isValidEdgeFunctionURL(url, false), `Expected ${url} to be valid`).toBe(true)
}
})

it('should not match invalid edge function URLs on platform', () => {
for (const url of invalidPlatformEdgeFunctionUrls) {
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be invalid`).toBe(false)
}
})

it('should not match invalid edge function URLs', () => {
it('should not match invalid edge function URLs off platform', () => {
for (const url of invalidEdgeFunctionUrls) {
expect(isValidEdgeFunctionURL(url), `Expected ${url} to be invalid`).toBe(false)
expect(isValidEdgeFunctionURL(url, false), `Expected ${url} to be invalid`).toBe(false)
}
})
})
10 changes: 9 additions & 1 deletion apps/studio/lib/api/edgeFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
const NIMBUS_PROD_PROJECTS_URL = process.env.NIMBUS_PROD_PROJECTS_URL

export const isValidEdgeFunctionURL = (url: string) => {
export const isValidEdgeFunctionURL = (url: string, isPlatform: boolean) => {
if (NIMBUS_PROD_PROJECTS_URL !== undefined) {
const apexDomain = NIMBUS_PROD_PROJECTS_URL.replace('https://*.', '').replace(/\./g, '\\.')
const nimbusRegex = new RegExp('^https://[a-z]*\\.' + apexDomain + '/functions/v[0-9]{1}/.*$')
return nimbusRegex.test(url)
}

if (!isPlatform) {
const regexValidLocalEdgeFunctionURL = new RegExp(
'^https?://[^\\s/?#]+/functions/v[0-9]{1}/.*$'
)

return regexValidLocalEdgeFunctionURL.test(url)
}

const regexValidEdgeFunctionURL = new RegExp(
'^https://[a-z]*.supabase.(red|co)/functions/v[0-9]{1}/.*$'
)
Expand Down
8 changes: 6 additions & 2 deletions apps/studio/pages/api/edge-functions/test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IS_PLATFORM } from 'common'
import { isValidEdgeFunctionURL } from 'lib/api/edgeFunctions'
import { NextApiRequest, NextApiResponse } from 'next'

Expand All @@ -20,9 +21,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)

async function handlePost(req: NextApiRequest, res: NextApiResponse) {
try {
const { url, method, body: requestBody, headers: customHeaders } = req.body
const { url: requestUrl, method, body: requestBody, headers: customHeaders } = req.body
const url = IS_PLATFORM
? requestUrl
: requestUrl.replace(process.env.SUPABASE_PUBLIC_URL, process.env.SUPABASE_URL)

const validEdgeFnUrl = isValidEdgeFunctionURL(url)
const validEdgeFnUrl = isValidEdgeFunctionURL(url, IS_PLATFORM)

if (!validEdgeFnUrl) {
return res.status(400).json({
Expand Down
70 changes: 35 additions & 35 deletions apps/studio/pages/project/[ref]/observability/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'

import { useParams, useFlag } from 'common'
import { CreateReportModal } from 'components/interfaces/Reports/CreateReportModal'
import { useFeatureFlags, useFlag, useParams } from 'common'
import { ObservabilityOverview } from 'components/interfaces/Observability/ObservabilityOverview'
import { CreateReportModal } from 'components/interfaces/Reports/CreateReportModal'
import DefaultLayout from 'components/layouts/DefaultLayout'
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import { useContentQuery } from 'data/content/content-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useProfile } from 'lib/profile'
import { useRouter } from 'next/router'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
import type { NextPageWithLayout } from 'types'
import { LogoLoader } from 'ui'
import { parseAsBoolean, useQueryState } from 'nuqs'

export const UserReportPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref } = useParams()

const { profile } = useProfile()
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const showOverview = useFlag('observabilityOverview')
const [showCreateReportModal, setShowCreateReportModal] = useQueryState(
'newReport',
Expand All @@ -37,14 +37,15 @@ export const UserReportPage: NextPageWithLayout = () => {

useEffect(() => {
if (!isSuccess) return
if (!flagsLoaded) return // Wait for flags to load before checking redirect
if (showOverview) return // Don't redirect if overview is enabled

const reports = data.content
.filter((x) => x.type === 'report')
.sort((a, b) => a.name.localeCompare(b.name))
if (reports.length >= 1) router.push(`/project/${ref}/observability/${reports[0].id}`)
if (reports.length === 0) router.push(`/project/${ref}/observability/api-overview`)
}, [isSuccess, data, router, ref, showOverview])
}, [isSuccess, data, router, ref, showOverview, flagsLoaded])

const { can: canCreateReport } = useAsyncCheckPermissions(
PermissionAction.CREATE,
Expand All @@ -55,41 +56,40 @@ export const UserReportPage: NextPageWithLayout = () => {
}
)

// Wait for flags to load before rendering to avoid flashing wrong page
if (!flagsLoaded || isLoading) {
return <LogoLoader />
}

// Show overview page if feature flag is enabled
if (showOverview) {
return <ObservabilityOverview />
}

return (
<div className="h-full w-full">
{isLoading ? (
<LogoLoader />
) : (
<>
<ProductEmptyState
title="Observability"
ctaButtonLabel="New custom report"
onClickCta={() => {
setShowCreateReportModal(true)
}}
disabled={!canCreateReport}
disabledMessage="You need additional permissions to create a report"
>
<p className="text-foreground-light text-sm">
Create custom reports for your projects.
</p>
<p className="text-foreground-light text-sm">
Get a high level overview of your network traffic, user actions, and infrastructure
health.
</p>
</ProductEmptyState>
<CreateReportModal
visible={showCreateReportModal}
onCancel={() => setShowCreateReportModal(false)}
afterSubmit={() => setShowCreateReportModal(false)}
/>
</>
)}
<>
<ProductEmptyState
title="Observability"
ctaButtonLabel="New custom report"
onClickCta={() => {
setShowCreateReportModal(true)
}}
disabled={!canCreateReport}
disabledMessage="You need additional permissions to create a report"
>
<p className="text-foreground-light text-sm">Create custom reports for your projects.</p>
<p className="text-foreground-light text-sm">
Get a high level overview of your network traffic, user actions, and infrastructure
health.
</p>
</ProductEmptyState>
<CreateReportModal
visible={showCreateReportModal}
onCancel={() => setShowCreateReportModal(false)}
afterSubmit={() => setShowCreateReportModal(false)}
/>
</>
</div>
)
}
Expand Down
Loading
Loading