Merge remote-tracking branch 'origin/master' into prototype/nav-concept-2

This commit is contained in:
kemal committed 2025-10-23 15:24:15 +01:00
commit 2ab1e8c05f
18 files changed
+204 -91

No files matched your search

@@ -31,7 +31,7 @@ hideToc: true
<StepHikeCompact.Details title="Create a Next.js app">
Use the `create-next-app` command and the `with-supabase` template, to create a Next.js app pre-configured with:
- [Cookie-based Auth](docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs&queryGroups=environment&environment=server)
- [Cookie-based Auth](/docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs&queryGroups=environment&environment=server)
- [TypeScript](https://www.typescriptlang.org/)
- [Tailwind CSS](https://tailwindcss.com/)
@@ -18,7 +18,7 @@ hideToc: true
<StepHikeCompact.Details title="Create a Next.js app">
Use the `create-next-app` command and the `with-supabase` template, to create a Next.js app pre-configured with:
- [Cookie-based Auth](docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs&queryGroups=environment&environment=server)
- [Cookie-based Auth](/docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs&queryGroups=environment&environment=server)
- [TypeScript](https://www.typescriptlang.org/)
- [Tailwind CSS](https://tailwindcss.com/)
@@ -227,9 +227,9 @@ Now, create the associated `src/routes/account/+page.server.ts` file that handle
and handle all form actions through the `actions` object.
<$CodeSample
path="/user-management/sveltekit-user-management/src/routes/+page.server.ts"
path="/user-management/sveltekit-user-management/src/routes/account/+page.server.ts"
lines={[[1, -1]]}
meta="name=src/routes/+page.server.ts"
meta="name=src/routes/account/+page.server.ts"
/>
### Launch!
@@ -2,8 +2,7 @@ import { cache } from 'react'
import { z } from 'zod'
import { cache_fullProcess_withDevCacheBust } from '~/features/helpers.fs'
import { IS_PLATFORM } from '~/lib/constants'
import { supabaseAdmin } from '~/lib/supabaseAdmin'
import { supabase } from '~/lib/supabase'
import {
getAllTroubleshootingEntriesInternal,
getArticleSlug as getArticleSlugInternal,
@@ -27,10 +26,9 @@ export interface ITroubleshootingEntry {
export const getArticleSlug = getArticleSlugInternal
async function getAllTroubleshootingEntriesTyped() {
const result: ITroubleshootingEntry[] = (
IS_PLATFORM ? await getAllTroubleshootingEntriesInternal() : []
) as ITroubleshootingEntry[]
return result
const result: ITroubleshootingEntry[] =
(await getAllTroubleshootingEntriesInternal()) as ITroubleshootingEntry[]
return result ?? []
}
export const getAllTroubleshootingEntries = cache_fullProcess_withDevCacheBust(
getAllTroubleshootingEntriesTyped,
@@ -91,15 +89,11 @@ export async function getAllTroubleshootingErrors() {
}
async function getTroubleshootingUpdatedDatesInternal() {
if (!IS_PLATFORM) {
return new Map<string, Date>()
}
const databaseIds = (await getAllTroubleshootingEntries())
.map((entry) => entry.data.database_id)
.filter((id) => !id.startsWith('pseudo-'))
const { data, error } = await supabaseAdmin()
const { data, error } = await supabase()
.from('troubleshooting_entries')
.select('id, date_updated')
.in('id', databaseIds)
+1
View File
@@ -1,4 +1,5 @@
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
import { type Database as DatabaseGenerated } from 'common'
export type Database = {
@@ -3,10 +3,20 @@ import {
ScaffoldSectionTitle,
ScaffoldSectionContent,
} from 'components/layouts/Scaffold'
import { Card, CardContent, CardHeader, CardTitle, cn, Skeleton } from 'ui'
import {
Card,
CardContent,
CardHeader,
CardTitle,
cn,
Skeleton,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import Link from 'next/link'
import { useParams } from 'common'
import { ChevronRight, ExternalLink } from 'lucide-react'
import { ChevronRight, ExternalLink, HelpCircle } from 'lucide-react'
import { Reports } from 'icons'
import {
getChangeColor,
@@ -28,6 +38,7 @@ export const StatCard = ({
suffix = '',
invert = false,
href,
tooltip,
}: {
title: string
current: number
@@ -36,6 +47,7 @@ export const StatCard = ({
suffix?: string
invert?: boolean
href?: string
tooltip?: string
}) => {
const router = useRouter()
const isZeroChange = previous === 0
@@ -53,7 +65,19 @@ export const StatCard = ({
return (
<Card className={cn(href, 'mb-0 flex flex-col')}>
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0 pb-0 border-b-0 relative">
<CardTitle className="text-foreground-light">{title}</CardTitle>
<CardTitle className="text-foreground-light flex items-center gap-2">
{title}
{tooltip && (
<Tooltip>
<TooltipTrigger>
<HelpCircle className="text-foreground-light" size={14} strokeWidth={1.5} />
</TooltipTrigger>
<TooltipContent className="w-[300px]">
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
)}
</CardTitle>
<ButtonTooltip
type="text"
size="tiny"
@@ -160,11 +184,12 @@ export const OverviewUsage = () => {
<ScaffoldSectionContent className="gap-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<StatCard
title="Active users"
title="Auth Activity" // https://supabase.slack.com/archives/C08N7894QTG/p1761210058358439?thread_ts=1761147906.491599&cid=C08N7894QTG
current={metrics.current.activeUsers}
previous={activeUsersChange}
loading={isLoading}
href={`/project/${ref}/reports/auth?its=${startDate}&ite=${endDate}#usage`}
tooltip="Users who generated any Auth event in this period. This metric tracks authentication activity, not total product usage. Some active users won't appear here if their session stayed valid."
/>
<StatCard
title="Sign ups"
@@ -30,9 +30,16 @@ import {
import { Row } from 'ui-patterns'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
export const AdvisorSection = () => {
export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: boolean }) => {
const { ref: projectRef } = useParams()
const { data: lints, isLoading: isLoadingLints } = useProjectLintsQuery({ projectRef })
const { data: lints, isLoading: isLoadingLints } = useProjectLintsQuery(
{
projectRef,
},
{
enabled: !showEmptyState,
}
)
const snap = useAiAssistantStateSnapshot()
const { mutate: sendEvent } = useSendEventMutation()
const { data: organization } = useSelectedOrganizationQuery()
@@ -93,6 +100,10 @@ export const AdvisorSection = () => {
[sendEvent, projectRef, organization]
)
if (showEmptyState) {
return <EmptyState />
}
return (
<div>
{isLoadingLints ? (
@@ -199,15 +210,21 @@ export const AdvisorSection = () => {
</Sheet>
</>
) : (
<Card className="bg-transparent">
<CardContent className="flex flex-col items-center justify-center gap-2 p-16">
<Shield size={20} strokeWidth={1.5} className="text-foreground-muted" />
<p className="text-sm text-foreground-light text-center">
No security or performance errors found
</p>
</CardContent>
</Card>
<EmptyState />
)}
</div>
)
}
function EmptyState() {
return (
<Card className="bg-transparent">
<CardContent className="flex flex-col items-center justify-center gap-2 p-16">
<Shield size={20} strokeWidth={1.5} className="text-foreground-muted" />
<p className="text-sm text-foreground-light text-center">
No security or performance errors found
</p>
</CardContent>
</Card>
)
}
@@ -31,6 +31,7 @@ export const HomeV2 = () => {
const hasShownEnableBranchingModalRef = useRef(false)
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
const isComingUp = project?.status === PROJECT_STATUS.COMING_UP
const [sectionOrder, setSectionOrder] = useLocalStorage<string[]>(
`home-section-order-${project?.ref || 'default'}`,
@@ -101,9 +102,11 @@ export const HomeV2 = () => {
{sectionOrder.map((id) => {
if (IS_PLATFORM && id === 'usage') {
return (
<SortableSection key={id} id={id}>
<ProjectUsageSection />
</SortableSection>
<div key={id} className={cn(isComingUp && 'opacity-60 pointer-events-none')}>
<SortableSection id={id}>
<ProjectUsageSection />
</SortableSection>
</div>
)
}
if (
@@ -123,16 +126,20 @@ export const HomeV2 = () => {
}
if (id === 'advisor') {
return (
<SortableSection key={id} id={id}>
<AdvisorSection />
</SortableSection>
<div key={id} className={cn(isComingUp && 'opacity-60 pointer-events-none')}>
<SortableSection id={id}>
<AdvisorSection showEmptyState={isComingUp} />
</SortableSection>
</div>
)
}
if (id === 'custom-report') {
return (
<SortableSection key={id} id={id}>
<CustomReportSection />
</SortableSection>
<div key={id} className={cn(isComingUp && 'opacity-60 pointer-events-none')}>
<SortableSection id={id}>
<CustomReportSection />
</SortableSection>
</div>
)
}
})}
@@ -254,36 +254,44 @@ export const ServiceStatus = () => {
(service) => !service.isHealthy && service.status !== 'COMING_UP'
)
const anyComingUp = services.some((service) => service.status === 'COMING_UP')
const overallStatusLabel = isLoadingChecks
? 'Checking...'
: anyUnhealthy && !isProjectNew
? 'Unhealthy'
: anyComingUp || isMigrationLoading || (isProjectNew && !allServicesOperational)
? 'Coming up...'
: 'Healthy'
// Spinner only while the overall project is in COMING_UP; otherwise show 6-dot grid
const showSpinnerIcon = project?.status === 'COMING_UP'
const getOverallStatusLabel = (): string => {
if (isLoadingChecks) return 'Checking...'
if (anyComingUp) return 'Coming up...'
if (anyUnhealthy) return 'Unhealthy'
return 'Healthy'
}
const overallStatusLabel = getOverallStatusLabel()
return (
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_>
<SingleStat
icon={
<div className="grid grid-cols-3 gap-1">
{services.map((service, index) => (
<div
key={`${service.name}-${index}`}
className={cn(
'w-1.5 h-1.5 rounded-full',
service.isLoading ||
service.status === 'COMING_UP' ||
(isProjectNew && !service.isHealthy)
? 'bg-foreground-lighter animate-pulse'
: service.isHealthy
? 'bg-brand'
: 'bg-selection'
)}
/>
))}
</div>
showSpinnerIcon ? (
<Loader2 className="animate-spin" size={18} />
) : (
<div className="grid grid-cols-3 gap-1">
{services.map((service, index) => (
<div
key={`${service.name}-${index}`}
className={cn(
'w-1.5 h-1.5 rounded-full',
service.isLoading ||
service.status === 'COMING_UP' ||
(isProjectNew && !service.isHealthy)
? 'bg-foreground-lighter animate-pulse'
: service.isHealthy
? 'bg-brand'
: 'bg-selection'
)}
/>
))}
</div>
)
}
label={<span>Status</span>}
value={<span>{overallStatusLabel}</span>}
@@ -47,12 +47,14 @@ const AwsMarketplaceCreateNewOrg = ({ onboardingInfo }: Props) => {
return (
<>
{onboardingInfo && !onboardingInfo.aws_contract_auto_renewal && (
<AwsMarketplaceAutoRenewalWarning
awsContractEndDate={onboardingInfo.aws_contract_end_date}
awsContractSettingsUrl={onboardingInfo.aws_contract_settings_url}
/>
)}
{onboardingInfo &&
!onboardingInfo.aws_contract_auto_renewal &&
!onboardingInfo.aws_contract_is_private_offer && (
<AwsMarketplaceAutoRenewalWarning
awsContractEndDate={onboardingInfo.aws_contract_end_date}
awsContractSettingsUrl={onboardingInfo.aws_contract_settings_url}
/>
)}
<ScaffoldSection>
<ScaffoldSectionDetail className="text-base">
<p>
@@ -62,8 +64,11 @@ const AwsMarketplaceCreateNewOrg = ({ onboardingInfo }: Props) => {
</p>
<p>
You can read more on billing through AWS in our {''}
{/*TODO(thomas): Update docs link once the new docs exist*/}
<Link href={`${DOCS_URL}/guides/platform`} target="_blank" className="underline">
<Link
href={`${DOCS_URL}/guides/platform/aws-marketplace`}
target="_blank"
className="underline"
>
Billing Docs.
</Link>
</p>
@@ -118,12 +118,14 @@ export const AwsMarketplaceLinkExistingOrg = ({
return (
<>
{onboardingInfo && !onboardingInfo.aws_contract_auto_renewal && (
<AwsMarketplaceAutoRenewalWarning
awsContractEndDate={onboardingInfo.aws_contract_end_date}
awsContractSettingsUrl={onboardingInfo.aws_contract_settings_url}
/>
)}
{onboardingInfo &&
!onboardingInfo.aws_contract_auto_renewal &&
!onboardingInfo.aws_contract_is_private_offer && (
<AwsMarketplaceAutoRenewalWarning
awsContractEndDate={onboardingInfo.aws_contract_end_date}
awsContractSettingsUrl={onboardingInfo.aws_contract_settings_url}
/>
)}
<ScaffoldSection>
<ScaffoldSectionDetail className="text-base">
<>
@@ -136,8 +138,11 @@ export const AwsMarketplaceLinkExistingOrg = ({
<p>
You can read more on billing through AWS in our {''}
{/*TODO(thomas): Update docs link once the new docs exist*/}
<Link href={`${DOCS_URL}/guides/platform`} target="_blank" className="underline">
<Link
href={`${DOCS_URL}/guides/platform/aws-marketplace`}
target="_blank"
className="underline"
>
Billing Docs.
</Link>
</p>
@@ -280,11 +280,14 @@ export const QueryPerformanceGrid = ({
}
if (col.id === 'cache_hit_rate') {
const numericValue = typeof value === 'number' ? value : parseFloat(value)
return (
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
<p className={cn(value.toFixed(2) === '0.00' && 'text-foreground-lighter')}>
{value.toLocaleString(undefined, {
{typeof numericValue === 'number' &&
!isNaN(numericValue) &&
isFinite(numericValue) ? (
<p className={cn(numericValue.toFixed(2) === '0.00' && 'text-foreground-lighter')}>
{numericValue.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
@@ -3,7 +3,7 @@ import Head from 'next/head'
import { useRouter } from 'next/router'
import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect, useState } from 'react'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams, useFlag } from 'common'
import { CreateBranchModal } from 'components/interfaces/BranchManagement/CreateBranchModal'
import ProjectAPIDocs from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
import { AIAssistant } from 'components/ui/AIAssistantPanel/AIAssistant'
@@ -318,11 +318,13 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp
const { ref } = useParams()
const state = useDatabaseSelectorStateSnapshot()
const { data: selectedProject } = useSelectedProjectQuery()
const isHomeNewFlag = useFlag('homeNew')
const isBranchesPage = router.pathname.includes('/project/[ref]/branches')
const isSettingsPages = router.pathname.includes('/project/[ref]/settings')
const isVaultPage = router.pathname === '/project/[ref]/settings/vault'
const isBackupsPage = router.pathname.includes('/project/[ref]/database/backups')
const isHomePage = router.pathname === '/project/[ref]'
const requiresDbConnection: boolean =
(!isSettingsPages && !routesToIgnoreDBConnection.includes(router.pathname)) || isVaultPage
@@ -343,6 +345,20 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp
const isProjectPauseFailed = selectedProject?.status === PROJECT_STATUS.PAUSE_FAILED
const isProjectOffline = selectedProject?.postgrestStatus === 'OFFLINE'
// handle redirect to home for building state
const shouldRedirectToHomeForBuilding =
isHomeNewFlag && requiresDbConnection && isProjectBuilding && !isBranchesPage && !isHomePage
// We won't be showing the building state with the new home page
const shouldShowBuildingState =
requiresDbConnection && isProjectBuilding && !isBranchesPage && !(isHomeNewFlag && isHomePage)
useEffect(() => {
if (shouldRedirectToHomeForBuilding && ref) {
router.replace(`/project/${ref}`)
}
}, [shouldRedirectToHomeForBuilding, ref, router])
useEffect(() => {
if (ref) state.setSelectedDatabaseId(ref)
}, [ref])
@@ -383,7 +399,11 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp
return <RestoreFailedState />
}
if (requiresDbConnection && isProjectBuilding && !isBranchesPage) {
if (shouldRedirectToHomeForBuilding) {
return <Loading />
}
if (shouldShowBuildingState) {
return <BuildingState />
}
@@ -8,7 +8,7 @@ describe('defaultAuthReportFormatter', () => {
const data = { result: [{ timestamp: String(timestamp), count: 1 }] }
const attributes = [
{ attribute: 'ActiveUsers', provider: 'logs', label: 'Active Users', enabled: true },
{ attribute: 'ActiveUsers', provider: 'logs', label: 'Auth Activity', enabled: true },
]
const result = defaultAuthReportFormatter(data, attributes)
@@ -28,7 +28,7 @@ describe('defaultAuthReportFormatter', () => {
}
const attributes = [
{ attribute: 'active_users', label: 'Active Users' },
{ attribute: 'active_users', label: 'Auth Activity' },
{ attribute: 'sign_in_attempts', label: 'Sign In Attempts' },
]
@@ -47,7 +47,7 @@ describe('defaultAuthReportFormatter', () => {
const data = { result: [] }
const attributes = [
{ attribute: 'ActiveUsers', provider: 'logs', label: 'Active Users', enabled: true },
{ attribute: 'ActiveUsers', provider: 'logs', label: 'Auth Activity', enabled: true },
]
const result = defaultAuthReportFormatter(data, attributes)
+4 -3
View File
@@ -513,7 +513,7 @@ export const createUsageReportConfig = ({
return [
{
id: 'active-user',
label: 'Active Users',
label: 'Auth Activity', // https://supabase.slack.com/archives/C08N7894QTG/p1761210058358439?thread_ts=1761147906.491599&cid=C08N7894QTG
valuePrecision: 0,
hide: false,
showTooltip: true,
@@ -521,11 +521,12 @@ export const createUsageReportConfig = ({
showMaxValue: false,
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of active users over time.',
titleTooltip:
"Users who generated any Auth event in this period. This metric tracks authentication activity, not total product usage. Some active users won't appear here if their session stayed valid.",
availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{ attribute: 'ActiveUsers', provider: 'logs', label: 'Active Users', enabled: true },
{ attribute: 'ActiveUsers', provider: 'logs', label: 'Auth Activity', enabled: true },
]
const sql = AUTH_REPORT_SQL.ActiveUsers(interval, filters)
+14 -2
View File
@@ -91,6 +91,8 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
// [Joshen] This page is getting rather big and complex, let's aim to break this down into smaller components
const sizes: DesiredInstanceSize[] = ['micro', 'small', 'medium']
const sizesWithNoCostConfirmationRequired: DesiredInstanceSize[] = ['micro', 'small']
@@ -146,6 +148,7 @@ const Wizard: NextPageWithLayout = () => {
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
const showPostgresVersionSelector = useFlag('showPostgresVersionSelector')
const cloudProviderEnabled = useFlag('enableFlyCloudProvider')
const isHomeNew = useFlag('homeNew')
const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config')
const { infraCloudProviders: validCloudProviders } = useCustomContent(['infra:cloud_providers'])
@@ -254,7 +257,7 @@ const Wizard: NextPageWithLayout = () => {
organization: res.organization_slug,
},
})
router.push(`/project/${res.ref}/building`)
router.push(isHomeNew ? `/project/${res.ref}` : `/project/${res.ref}/building`)
},
})
@@ -284,7 +287,7 @@ const Wizard: NextPageWithLayout = () => {
useOrganizationAvailableRegionsQuery(
{
slug: slug,
cloudProvider: PROVIDERS[defaultProvider].id,
cloudProvider: PROVIDERS[cloudProvider as CloudProvider].id,
desiredInstanceSize: instanceSize as DesiredInstanceSize,
},
{
@@ -295,6 +298,9 @@ const Wizard: NextPageWithLayout = () => {
refetchOnReconnect: false,
}
)
const recommendedSmartRegion = smartRegionEnabled
? availableRegionsData?.recommendations.smartGroup.name
: undefined
const regionError =
smartRegionEnabled && defaultProvider !== 'AWS_NIMBUS'
? availableRegionsError
@@ -473,6 +479,12 @@ const Wizard: NextPageWithLayout = () => {
}
}, [regionError])
useEffect(() => {
if (recommendedSmartRegion) {
form.setValue('dbRegion', recommendedSmartRegion)
}
}, [recommendedSmartRegion])
useEffect(() => {
if (watchedInstanceSize !== instanceSize) {
form.setValue('instanceSize', instanceSize, {
+1
View File
@@ -4679,6 +4679,7 @@ export interface components {
CloudMarketplaceOnboardingInfoResponse: {
aws_contract_auto_renewal: boolean
aws_contract_end_date: string
aws_contract_is_private_offer: boolean
aws_contract_settings_url: string
aws_contract_start_date: string
organization_linking_eligibility: {
@@ -0,0 +1,14 @@
-- Give anon and authenticated read access to the troubleshooting entries table
-- Allows troubleshooting entries to be generated in local dev
create policy anon_read_troubleshooting_entries
on public.troubleshooting_entries
for select
to anon
using (true);
create policy authenticated_read_troubleshooting_entries
on public.troubleshooting_entries
for select
to authenticated
using (true);