From f7009952f92701f8bcebaf61374ce3cc080c88f7 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 29 Jul 2025 18:16:11 +0800 Subject: [PATCH 1/9] Fix tabs valtio store initial render (#37505) * Fix tabs valtio store initial render * Address feedback --------- Co-authored-by: Ivan Vasilov --- apps/studio/state/tabs.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/studio/state/tabs.tsx b/apps/studio/state/tabs.tsx index 59f9bdd8c44e3..2d6fda0103554 100644 --- a/apps/studio/state/tabs.tsx +++ b/apps/studio/state/tabs.tsx @@ -1,9 +1,8 @@ -import { useConstant } from 'common' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' -import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { partition } from 'lodash' import { NextRouter } from 'next/router' -import { createContext, PropsWithChildren, ReactNode, useContext, useEffect } from 'react' +import { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useState } from 'react' import { proxy, subscribe, useSnapshot } from 'valtio' export const editorEntityTypes = { @@ -78,6 +77,7 @@ const DEFAULT_TABS_STATE = { openTabs: [] as string[], tabsMap: {} as { [key: string]: Tab }, previewTabId: undefined as string | undefined, + recentItems: [], } const TABS_STORAGE_KEY = 'supabase_studio_tabs' const getTabsStorageKey = (ref: string) => `${TABS_STORAGE_KEY}_${ref}` @@ -399,8 +399,14 @@ export type TabsState = ReturnType export const TabsStateContext = createContext(createTabsState('')) export const TabsStateContextProvider = ({ children }: PropsWithChildren) => { - const project = useSelectedProject() - const state = useConstant(() => createTabsState(project?.ref ?? '')) + const { data: project } = useSelectedProjectQuery() + const [state, setState] = useState(createTabsState(project?.ref ?? '')) + + useEffect(() => { + if (typeof window !== 'undefined' && !!project?.ref) { + setState(createTabsState(project?.ref ?? '')) + } + }, [project?.ref]) useEffect(() => { if (typeof window !== 'undefined' && project?.ref) { @@ -422,8 +428,7 @@ export const TabsStateContextProvider = ({ children }: PropsWithChildren) => { ) }) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [project?.ref, state]) return {children} } From 375866e700218c70a3278fb72013757e90e5e5ed Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Tue, 29 Jul 2025 12:23:38 +0200 Subject: [PATCH 2/9] Add a warning if no policies on realtime.messages when turning on private mode (#37512) * Add a warning if no policies on realtime.messages when turning on private mode. * Minor copy fixes. * Update warning copy --------- Co-authored-by: Joshen Lim --- .../interfaces/Realtime/RealtimeSettings.tsx | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx index 87c29b77990b7..4553a4ee9a14e 100644 --- a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx +++ b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx @@ -1,5 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' +import Link from 'next/link' import { useEffect } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' @@ -11,6 +12,7 @@ import { ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' import { InlineLink } from 'components/ui/InlineLink' +import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' import { useRealtimeConfigurationUpdateMutation } from 'data/realtime/realtime-config-mutation' import { @@ -50,8 +52,21 @@ export const RealtimeSettings = () => { projectRef, }) + const { data: policies } = useDatabasePoliciesQuery({ + projectRef, + connectionString: project?.connectionString, + schema: 'realtime', + }) + const isUsageBillingEnabled = organization?.usage_billing_enabled + // Check if RLS policies exist for realtime.messages table + const realtimeMessagesPolicies = policies?.filter( + (policy) => policy.schema === 'realtime' && policy.table === 'messages' + ) + const hasRealtimeMessagesPolicies = + realtimeMessagesPolicies && realtimeMessagesPolicies.length > 0 + const { mutate: updateRealtimeConfig, isLoading: isUpdatingConfig } = useRealtimeConfigurationUpdateMutation({ onSuccess: () => { @@ -83,6 +98,9 @@ export const RealtimeSettings = () => { }, }) + const { allow_public } = form.watch() + const isSettingToPrivate = !data?.private_only && !allow_public + const onSubmit: SubmitHandler> = (data) => { if (!projectRef) return console.error('Project ref is required') updateRealtimeConfig({ @@ -132,6 +150,30 @@ export const RealtimeSettings = () => { /> + + {!hasRealtimeMessagesPolicies && !allow_public && ( + +

+ Private mode is {isSettingToPrivate ? 'being ' : ''} + enabled, but no RLS policies exists on the{' '} + realtime.messages table. No + messages will be received by users. +

+ + + + } + /> + )} )} From 51d22de62e95f4c005a8c529d2aabac4d0a8b2cd Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Tue, 29 Jul 2025 11:52:22 +0100 Subject: [PATCH 3/9] docs(ui-library): fix oauth example code to use exchangeCodeForSession (#36311) * docs(ui-library): fix oauth example code to use exchangeCodeForSession * Build the registry item for social auth tanstack. --------- Co-authored-by: Ivan Vasilov --- .../public/r/social-auth-tanstack.json | 2 +- .../social-auth-tanstack/routes/auth/oauth.ts | 17 +++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/apps/ui-library/public/r/social-auth-tanstack.json b/apps/ui-library/public/r/social-auth-tanstack.json index 5304e77f91ae1..ccc42a24aae97 100644 --- a/apps/ui-library/public/r/social-auth-tanstack.json +++ b/apps/ui-library/public/r/social-auth-tanstack.json @@ -43,7 +43,7 @@ }, { "path": "registry/default/blocks/social-auth-tanstack/routes/auth/oauth.ts", - "content": "import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'\nimport { type EmailOtpType } from '@supabase/supabase-js'\nimport { createFileRoute, redirect } from '@tanstack/react-router'\nimport { createServerFn } from '@tanstack/react-start'\nimport { getWebRequest } from '@tanstack/react-start/server'\n\nconst confirmFn = createServerFn({ method: 'GET' })\n .validator((searchParams: unknown) => {\n if (\n searchParams &&\n typeof searchParams === 'object' &&\n 'token_hash' in searchParams &&\n 'type' in searchParams &&\n 'next' in searchParams\n ) {\n return searchParams\n }\n throw new Error('Invalid search params')\n })\n .handler(async (ctx) => {\n const request = getWebRequest()\n\n if (!request) {\n throw redirect({ to: `/auth/error`, search: { error: 'No request' } })\n }\n\n const searchParams = ctx.data\n const token_hash = searchParams['token_hash'] as string\n const type = searchParams['type'] as EmailOtpType | null\n const _next = (searchParams['next'] ?? '/') as string\n const next = _next?.startsWith('/') ? _next : '/'\n\n if (token_hash && type) {\n const supabase = createClient()\n\n const { error } = await supabase.auth.verifyOtp({\n type,\n token_hash,\n })\n console.log(error?.message)\n if (!error) {\n // redirect user to specified redirect URL or root of app\n throw redirect({ href: next })\n } else {\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: error?.message },\n })\n }\n }\n\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: 'No token hash or type' },\n })\n })\n\nexport const Route = createFileRoute('/auth/confirm')({\n preload: false,\n loader: (opts) => confirmFn({ data: opts.location.search }),\n})\n", + "content": "import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'\nimport { createFileRoute, redirect } from '@tanstack/react-router'\nimport { createServerFn } from '@tanstack/react-start'\nimport { getWebRequest } from '@tanstack/react-start/server'\n\nconst confirmFn = createServerFn({ method: 'GET' })\n .validator((searchParams: unknown) => {\n if (\n searchParams &&\n typeof searchParams === 'object' &&\n 'code' in searchParams &&\n 'next' in searchParams\n ) {\n return searchParams\n }\n throw new Error('Invalid search params')\n })\n .handler(async (ctx) => {\n const request = getWebRequest()\n\n if (!request) {\n throw redirect({ to: `/auth/error`, search: { error: 'No request' } })\n }\n\n const searchParams = ctx.data\n const code = searchParams['code'] as string\n const _next = (searchParams['next'] ?? '/') as string\n const next = _next?.startsWith('/') ? _next : '/'\n\n if (code) {\n const supabase = createClient()\n\n const { error } = await supabase.auth.exchangeCodeForSession(code)\n if (!error) {\n // redirect user to specified redirect URL or root of app\n throw redirect({ href: next })\n } else {\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: error?.message },\n })\n }\n }\n\n // redirect the user to an error page with some instructions\n throw redirect({\n to: `/auth/error`,\n search: { error: 'No code found' },\n })\n })\n\nexport const Route = createFileRoute('/auth/confirm')({\n preload: false,\n loader: (opts) => confirmFn({ data: opts.location.search }),\n})\n", "type": "registry:file", "target": "routes/auth/oauth.ts" }, diff --git a/apps/ui-library/registry/default/blocks/social-auth-tanstack/routes/auth/oauth.ts b/apps/ui-library/registry/default/blocks/social-auth-tanstack/routes/auth/oauth.ts index 2996a594a653b..529a6e539ab40 100644 --- a/apps/ui-library/registry/default/blocks/social-auth-tanstack/routes/auth/oauth.ts +++ b/apps/ui-library/registry/default/blocks/social-auth-tanstack/routes/auth/oauth.ts @@ -1,5 +1,4 @@ import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server' -import { type EmailOtpType } from '@supabase/supabase-js' import { createFileRoute, redirect } from '@tanstack/react-router' import { createServerFn } from '@tanstack/react-start' import { getWebRequest } from '@tanstack/react-start/server' @@ -9,8 +8,7 @@ const confirmFn = createServerFn({ method: 'GET' }) if ( searchParams && typeof searchParams === 'object' && - 'token_hash' in searchParams && - 'type' in searchParams && + 'code' in searchParams && 'next' in searchParams ) { return searchParams @@ -25,19 +23,14 @@ const confirmFn = createServerFn({ method: 'GET' }) } const searchParams = ctx.data - const token_hash = searchParams['token_hash'] as string - const type = searchParams['type'] as EmailOtpType | null + const code = searchParams['code'] as string const _next = (searchParams['next'] ?? '/') as string const next = _next?.startsWith('/') ? _next : '/' - if (token_hash && type) { + if (code) { const supabase = createClient() - const { error } = await supabase.auth.verifyOtp({ - type, - token_hash, - }) - console.log(error?.message) + const { error } = await supabase.auth.exchangeCodeForSession(code) if (!error) { // redirect user to specified redirect URL or root of app throw redirect({ href: next }) @@ -53,7 +46,7 @@ const confirmFn = createServerFn({ method: 'GET' }) // redirect the user to an error page with some instructions throw redirect({ to: `/auth/error`, - search: { error: 'No token hash or type' }, + search: { error: 'No code found' }, }) }) From 1cccc8e1066ef591bb4a0856772734c0ac869dcf Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 29 Jul 2025 20:55:49 +0800 Subject: [PATCH 4/9] Adjust layout of UserImpersonationSelector (#37533) * Adjust layout of UserImpersonationSelector * Small patch --- .../RoleImpersonationPopover.tsx | 2 +- .../RoleImpersonationSelector.tsx | 4 +- .../UserImpersonationSelector.tsx | 468 +++++++++--------- 3 files changed, 236 insertions(+), 238 deletions(-) diff --git a/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx b/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx index 971d7ee7136f2..551e5ef2ac1e0 100644 --- a/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx +++ b/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx @@ -73,7 +73,7 @@ const RoleImpersonationPopover = ({ diff --git a/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationSelector.tsx b/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationSelector.tsx index fa591e1e8a029..80a35929e3654 100644 --- a/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationSelector.tsx +++ b/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationSelector.tsx @@ -131,9 +131,7 @@ const RoleImpersonationSelector = ({ {selectedOption === 'authenticated' && ( <> -
- -
+ )} diff --git a/apps/studio/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx b/apps/studio/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx index 970cefaeb09de..edc0e0c753c89 100644 --- a/apps/studio/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx +++ b/apps/studio/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx @@ -1,11 +1,12 @@ import { useDebounce } from '@uidotdev/usehooks' -import { ChevronDown, ExternalLink, User as IconUser, Loader2, Search, X } from 'lucide-react' +import { ChevronDown, User as IconUser, Loader2, Search, X } from 'lucide-react' import { useMemo, useState } from 'react' import { toast } from 'sonner' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import AlertError from 'components/ui/AlertError' +import { InlineLink } from 'components/ui/InlineLink' import { User, useUsersInfiniteQuery } from 'data/auth/users-infinite-query' import { useCustomAccessTokenHookDetails } from 'hooks/misc/useCustomAccessTokenHookDetails' import { useLocalStorage } from 'hooks/misc/useLocalStorage' @@ -13,13 +14,18 @@ import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-stat import { ResponseError } from 'types' import { Button, + cn, Collapsible_Shadcn_, CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_, + DropdownMenuSeparator, Input, - Switch, ScrollArea, - cn, + Switch, + Tabs_Shadcn_, + TabsContent_Shadcn_, + TabsList_Shadcn_, + TabsTrigger_Shadcn_, } from 'ui' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { getAvatarUrl, getDisplayName } from '../Auth/Users/Users.utils' @@ -33,13 +39,13 @@ const UserImpersonationSelector = () => { const [additionalClaims, setAdditionalClaims] = useState('') const { id: tableId } = useParams() + const [selectedTab, setSelectedTab] = useState<'user' | 'external'>('user') const [previousSearches, setPreviousSearches] = useLocalStorage( LOCAL_STORAGE_KEYS.USER_IMPERSONATION_SELECTOR_PREVIOUS_SEARCHES(tableId!), [] ) - const [showExternalAuth, setShowExternalAuth] = useState(false) const state = useRoleImpersonationStateSnapshot() const debouncedSearchText = useDebounce(searchText, 300) @@ -146,7 +152,6 @@ const UserImpersonationSelector = () => { function stopImpersonating() { state.setRole(undefined) - setShowExternalAuth(false) // Reset external auth impersonation when stopping impersonation } function toggleAalState() { @@ -167,251 +172,246 @@ const UserImpersonationSelector = () => { setPreviousSearches([]) } - // Select a previous search - function selectPreviousSearch(prevUser: User) { - setSearchText(prevUser.email ?? prevUser.phone ?? prevUser.id ?? '') - } - return ( -
-

- {displayName ? `Impersonating ${displayName}` : 'Impersonate a User'} -

-

- {!impersonatingUser && !isExternalAuthImpersonating - ? "Select a user to respect your database's Row-Level Security policies for that particular user." - : "Results will respect your database's Row-Level Security policies for this user."} -

- - {/* Check for both regular user and external auth impersonation since they use different data structures but both need to be handled for displaying impersonation UI */} - {!impersonatingUser && !isExternalAuthImpersonating ? ( -
- - ) : ( - - ) - } - placeholder="Search by id, email, phone, or name..." - onChange={(e) => setSearchText(e.target.value)} - value={searchText} - size="small" - actions={ - searchText && ( - - ) - } + <> +
+

+ {displayName ? `Impersonating ${displayName}` : 'Impersonate a User'} +

+

+ {!impersonatingUser && !isExternalAuthImpersonating + ? "Select a user to respect your database's Row-Level Security policies for that particular user." + : "Results will respect your database's Row-Level Security policies for this user."} +

+ + {impersonatingUser && ( + + )} + {isExternalAuthImpersonating && ( + + )} - - -
-

- Advanced options -

- -
-
- -
-
-

MFA assurance level

- -

- AAL1 verifies users via standard login methods, while AAL2 adds a second - authentication factor. -
- If you're not using MFA, you can leave this on AAL1. -

- - Learn more about MFA - -
-
- -
-

AAL1

- -

AAL2

-
-
- + {!impersonatingUser && !isExternalAuthImpersonating && ( + setSelectedTab(value)}> + + Project user + + External user + + Test RLS policies with external auth providers like Clerk or Auth0 by providing a + user ID and optional claims. + + + + +
-
-

External Auth Impersonation

- -

- Test RLS policies with external auth providers like Clerk or Auth0 by - providing a user ID and optional claims. -

-
-
- -
- -

- Enable external auth impersonation -

-
- - {showExternalAuth && ( -
- setExternalUserId(e.target.value)} - size="small" - /> - setAdditionalClaims(e.target.value)} - size="small" - /> -
-
+ + ) : ( + + ) + } + placeholder="Search by id, email, phone, or name..." + onChange={(e) => setSearchText(e.target.value)} + value={searchText} + actions={ + searchText && ( -
+ ) + } + /> + {isLoading && ( +
+ + Loading users...
)} -
- - - {!showExternalAuth && ( - <> - {isLoading && ( -
- - Loading users... -
- )} - - {isError && } - - {isSuccess && - (users.length > 0 ? ( -
-
    - {users.map((user) => ( -
  • - -
  • - ))} -
-
- ) : ( -
-

- No users found -

-
- ))} - {previousSearches.length > 0 && ( -
- {previousSearches.length > 0 ? ( - <> - - -
-

- Recents -

- } + + {isSuccess && + (users.length > 0 ? ( +
+
    + {users.map((user) => ( +
  • + -
- - - - - 3 ? 'h-36' : 'h-auto')} - > -
    - {previousSearches.map((search) => ( -
  • - -
  • - ))} -
-
-
- - + + ))} + +
) : ( -
No recent searches
+
+

+ No users found +

+
+ ))} + + <> + {previousSearches.length > 0 && ( +
+ {previousSearches.length > 0 ? ( + <> + + +
+

+ Recents +

+ +
+
+ + + + 3 ? 'h-36' : 'h-auto')} + > +
    + {previousSearches.map((search) => ( +
  • + +
  • + ))} +
+
+
+
+ + ) : ( +
+ No recent searches +
+ )} +
)} + +
+ + + +
+ setExternalUserId(e.target.value)} + /> + setAdditionalClaims(e.target.value)} + /> +
+
- )} - - )} -
- ) : ( +
+ + + )} +
+ + {/* Check for both regular user and external auth impersonation since they use different data structures but both need to be handled for displaying impersonation UI */} + {!impersonatingUser && !isExternalAuthImpersonating ? ( <> - {impersonatingUser && ( - - )} - {isExternalAuthImpersonating && ( - - )} + +
+ + +
+

+ Advanced options +

+ +
+
+ +
+
+

MFA assurance level

+ + AAL1 verifies users via standard login methods, while AAL2 adds a second + authentication factor. If you're not using MFA, you can leave this on AAL1. + Learn more about MFA{' '} + + here + + . + +
+ +
+

AAL1

+ +

AAL2

+
+
+
+
+
- )} -
+ ) : null} + ) } From 6d0011449bf83ef4d9084cc06bf4ce9a2bdaec8f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 29 Jul 2025 22:14:45 +0800 Subject: [PATCH 5/9] Fix Authorization layout alignment (#37538) --- apps/studio/components/layouts/APIAuthorizationLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/layouts/APIAuthorizationLayout.tsx b/apps/studio/components/layouts/APIAuthorizationLayout.tsx index 10a57ab76b752..bebbb882323d0 100644 --- a/apps/studio/components/layouts/APIAuthorizationLayout.tsx +++ b/apps/studio/components/layouts/APIAuthorizationLayout.tsx @@ -14,7 +14,7 @@ const APIAuthorizationLayout = ({ children }: PropsWithChildren Authorize API access | Supabase -
+
From 4534e0b293daab564c8a3bac3e6a8f45e2923d0b Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:19:50 +0200 Subject: [PATCH 6/9] fix filter position (#37535) fix --- .../Realtime/Inspector/RealtimeFilterPopover/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx index 71a229b4660bd..e9d6cbcca8077 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx @@ -69,7 +69,7 @@ export const RealtimeFilterPopover = ({ config, onChangeConfig }: RealtimeFilter )} - +
Listen to event types
From c386daf3a0fc4d53ff919a1e372901168b685529 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:20:05 +0200 Subject: [PATCH 7/9] add max cpu usage chart (#37532) add max cpu usage --- .../components/ui/Charts/ComposedChart.tsx | 5 +++- apps/studio/data/reports/database-charts.ts | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/ui/Charts/ComposedChart.tsx b/apps/studio/components/ui/Charts/ComposedChart.tsx index 78e1e45a09c7b..5f891992832c2 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.tsx @@ -235,7 +235,10 @@ export default function ComposedChart({ }) : [] - const stackedAttributes = chartData.filter((att) => !att.name.includes('max')) + const stackedAttributes = chartData.filter((att) => { + const attribute = attributes.find((attr) => attr.attribute === att.name) + return !attribute?.isMaxValue + }) const isPercentage = format === '%' const isRamChart = !chartData?.some((att: any) => att.name.toLowerCase() === 'ram_usage') && diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts index d57f7a0c4ee45..1f38e714f72a7 100644 --- a/apps/studio/data/reports/database-charts.ts +++ b/apps/studio/data/reports/database-charts.ts @@ -34,7 +34,7 @@ export const getReportAttributes = (org: Organization, project: Project): Report }, { id: 'avg_cpu_usage', - label: 'CPU usage', + label: 'Average CPU usage', syncId: 'database-reports', format: '%', valuePrecision: 2, @@ -56,6 +56,30 @@ export const getReportAttributes = (org: Organization, project: Project): Report }, ], }, + { + id: 'max_cpu_usage', + label: 'Max CPU usage', + syncId: 'database-reports', + format: '%', + valuePrecision: 2, + availableIn: ['free', 'pro'], + hide: false, + showTooltip: false, + showLegend: false, + showMaxValue: false, + showGrid: false, + hideChartType: false, + defaultChartStyle: 'bar', + attributes: [ + { + attribute: 'max_cpu_usage', + provider: 'infra-monitoring', + label: 'Max CPU usage', + format: '%', + tooltip: 'Max CPU usage', + }, + ], + }, { id: 'disk-iops', label: 'Disk Input/Output operations per second (IOPS)', From 87fceafaebbb03dafe55c2d6ab4802fb15e6c548 Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:32:14 +0200 Subject: [PATCH 8/9] Docs: clarifying PITR behavior when disabled (#37539) --- apps/docs/content/guides/platform/backups.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/docs/content/guides/platform/backups.mdx b/apps/docs/content/guides/platform/backups.mdx index 64efb201ba51b..fe2774c4a8770 100644 --- a/apps/docs/content/guides/platform/backups.mdx +++ b/apps/docs/content/guides/platform/backups.mdx @@ -131,6 +131,14 @@ If you enable PITR, Daily Backups will no longer be taken. PITR provides a finer +When you disable PITR, all new backups will still be taken as physical backups only. Physical backups can still be used for restoration, but they are not available for direct download. If you need to download a backup after PITR is disabled, you’ll need to take a manual logical backup using the Supabase CLI or pg_dump. + + + +If PITR has been disabled, logical backups remain available until they pass the backup retention period for your plan. After that window passes, only physical backups will be shown. + + + ### Backup process [#pitr-backup-process] As discussed [here](https://supabase.com/blog/postgresql-physical-logical-backups), PITR is made possible by a combination of taking physical backups of a project, as well as archiving [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) files. Physical backups provide a snapshot of the underlying directory of the database, while WAL files contain records of every change made in the database. From 0a985a56433d6ceab6027e6aa5e86c3cb3cc8042 Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:54:42 +0200 Subject: [PATCH 9/9] Docs: add link to supabase db dump in PITR guide (#37541) --- apps/docs/content/guides/platform/backups.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/backups.mdx b/apps/docs/content/guides/platform/backups.mdx index fe2774c4a8770..ae79d74e7cf8f 100644 --- a/apps/docs/content/guides/platform/backups.mdx +++ b/apps/docs/content/guides/platform/backups.mdx @@ -131,7 +131,7 @@ If you enable PITR, Daily Backups will no longer be taken. PITR provides a finer -When you disable PITR, all new backups will still be taken as physical backups only. Physical backups can still be used for restoration, but they are not available for direct download. If you need to download a backup after PITR is disabled, you’ll need to take a manual logical backup using the Supabase CLI or pg_dump. +When you disable PITR, all new backups will still be taken as physical backups only. Physical backups can still be used for restoration, but they are not available for direct download. If you need to download a backup after PITR is disabled, you’ll need to take a manual [logical backup using the Supabase CLI or pg_dump](https://supabase.com/docs/guides/platform/migrating-within-supabase/backup-restore#backup-database-using-the-cli).