From ee5d4a9314b25a43a14960665f91b1fe97388cda Mon Sep 17 00:00:00 2001 From: Samir Ketema <6003000+samirketema@users.noreply.github.com> Date: Mon, 4 May 2026 16:24:44 -0700 Subject: [PATCH 1/5] chore: remove format param from audit log query (#45466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Cleanup after shipping https://github.com/supabase/supabase/pull/45389, the backend is now defaulting to the new v2 `format`, and made `format` param optional. So this: - removes references to `v2` naming, as this is the only format - removes the `format` query param from the audit logs API calls ## What is the current behavior? Same audit log functionality shown in https://github.com/supabase/supabase/pull/45389 ## What is the new behavior? Functionally the same behavior for audit logs. - [x] Manual test in staging ## Additional context ⚠️ Will leave the `do-not-merge` tag on until: - [ ] backend `format` optional PR lands in production. ## Summary by CodeRabbit * **Refactor** * Consolidated audit log type definitions and updated internal API request formatting for audit endpoints across Account and Organization audit log components. No changes to user-facing functionality or audit log display. --- .../studio/components/interfaces/Account/AuditLogs.tsx | 4 ++-- .../components/interfaces/Account/AuditLogs.utils.ts | 6 +++--- .../interfaces/AuditLogs/LogDetailsPanel.tsx | 4 ++-- .../Organization/AuditLogs/AuditLogs.utils.ts | 8 ++++---- .../organizations/organization-audit-logs-query.ts | 10 ++++------ apps/studio/data/profile/profile-audit-logs-query.ts | 5 ++--- .../tests/components/AuditLogs/AuditLogs.utils.test.ts | 4 ++-- 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/studio/components/interfaces/Account/AuditLogs.tsx b/apps/studio/components/interfaces/Account/AuditLogs.tsx index 481f8eaebcf54..c8058b898734d 100644 --- a/apps/studio/components/interfaces/Account/AuditLogs.tsx +++ b/apps/studio/components/interfaces/Account/AuditLogs.tsx @@ -17,7 +17,7 @@ import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { FilterPopover } from '@/components/ui/FilterPopover' import { TIMESTAMP_MICROS_PER_MS, - type V2AuditLog, + type AuditLog, } from '@/data/organizations/organization-audit-logs-query' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { useProfileAuditLogsQuery } from '@/data/profile/profile-audit-logs-query' @@ -35,7 +35,7 @@ export const AuditLogs = () => { to: currentTime.toISOString(), }) - const [selectedLog, setSelectedLog] = useState() + const [selectedLog, setSelectedLog] = useState() const [filters, setFilters] = useState<{ projects: string[] }>({ projects: [], }) diff --git a/apps/studio/components/interfaces/Account/AuditLogs.utils.ts b/apps/studio/components/interfaces/Account/AuditLogs.utils.ts index 081a6376427b8..2f19f1f96a1ac 100644 --- a/apps/studio/components/interfaces/Account/AuditLogs.utils.ts +++ b/apps/studio/components/interfaces/Account/AuditLogs.utils.ts @@ -1,12 +1,12 @@ -import type { V2AuditLog } from '@/data/organizations/organization-audit-logs-query' +import type { AuditLog } from '@/data/organizations/organization-audit-logs-query' -export function sortAuditLogs(logs: V2AuditLog[], descending: boolean): V2AuditLog[] { +export function sortAuditLogs(logs: AuditLog[], descending: boolean): AuditLog[] { return [...logs].sort((a, b) => descending ? b.timestamp - a.timestamp : a.timestamp - b.timestamp ) } -export function filterByProjects(logs: V2AuditLog[], projectRefs: string[]): V2AuditLog[] { +export function filterByProjects(logs: AuditLog[], projectRefs: string[]): AuditLog[] { if (projectRefs.length === 0) return logs return logs.filter((log) => projectRefs.includes(log.project_ref ?? '')) } diff --git a/apps/studio/components/interfaces/AuditLogs/LogDetailsPanel.tsx b/apps/studio/components/interfaces/AuditLogs/LogDetailsPanel.tsx index 7d66e81a246ea..e737884761680 100644 --- a/apps/studio/components/interfaces/AuditLogs/LogDetailsPanel.tsx +++ b/apps/studio/components/interfaces/AuditLogs/LogDetailsPanel.tsx @@ -8,11 +8,11 @@ import { } from '@/components/ui/Forms/FormSection' import { TIMESTAMP_MICROS_PER_MS, - type V2AuditLog, + type AuditLog, } from '@/data/organizations/organization-audit-logs-query' interface LogDetailsPanelProps { - selectedLog?: V2AuditLog + selectedLog?: AuditLog onClose: () => void } diff --git a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.utils.ts b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.utils.ts index a1ed75507bd04..92a61306543b7 100644 --- a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.utils.ts +++ b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.utils.ts @@ -1,20 +1,20 @@ import dayjs from 'dayjs' import { DatePickerToFrom } from '@/components/interfaces/Settings/Logs/Logs.types' -import type { V2AuditLog } from '@/data/organizations/organization-audit-logs-query' +import type { AuditLog } from '@/data/organizations/organization-audit-logs-query' -export function sortAuditLogs(logs: V2AuditLog[], descending: boolean): V2AuditLog[] { +export function sortAuditLogs(logs: AuditLog[], descending: boolean): AuditLog[] { return [...logs].sort((a, b) => descending ? b.timestamp - a.timestamp : a.timestamp - b.timestamp ) } -export function filterByUsers(logs: V2AuditLog[], userIds: string[]): V2AuditLog[] { +export function filterByUsers(logs: AuditLog[], userIds: string[]): AuditLog[] { if (userIds.length === 0) return logs return logs.filter((log) => userIds.includes(log.actor.user_id ?? '')) } -export function filterByProjects(logs: V2AuditLog[], projectRefs: string[]): V2AuditLog[] { +export function filterByProjects(logs: AuditLog[], projectRefs: string[]): AuditLog[] { if (projectRefs.length === 0) return logs return logs.filter((log) => projectRefs.includes(log.project_ref ?? '')) } diff --git a/apps/studio/data/organizations/organization-audit-logs-query.ts b/apps/studio/data/organizations/organization-audit-logs-query.ts index eea95980cab58..77870c3c7b102 100644 --- a/apps/studio/data/organizations/organization-audit-logs-query.ts +++ b/apps/studio/data/organizations/organization-audit-logs-query.ts @@ -4,11 +4,11 @@ import { organizationKeys } from './keys' import { get, handleError } from '@/data/fetchers' import type { ResponseError, UseCustomQueryOptions } from '@/types' -// V2 audit log timestamps are returned in microseconds, not milliseconds. +// Audit log timestamps are returned in microseconds, not milliseconds. // Divide by this constant before passing to dayjs/Date to get a valid date. export const TIMESTAMP_MICROS_PER_MS = 1000 -export type V2AuditLog = { +export type AuditLog = { organization_slug?: string project_ref?: string request_id: string @@ -33,10 +33,8 @@ export type V2AuditLog = { timestamp: number } -export type { V2AuditLog as AuditLog } - export type OrganizationAuditLogsResponse = { - result: V2AuditLog[] + result: AuditLog[] retention_period: number } @@ -53,7 +51,7 @@ export async function getOrganizationAuditLogs( if (!slug) throw new Error('slug is required') const { data, error } = await get('/platform/organizations/{slug}/audit', { - params: { path: { slug }, query: { iso_timestamp_start, iso_timestamp_end, format: 'v2' } }, + params: { path: { slug }, query: { iso_timestamp_start, iso_timestamp_end } }, signal, }) diff --git a/apps/studio/data/profile/profile-audit-logs-query.ts b/apps/studio/data/profile/profile-audit-logs-query.ts index 900e12ba152c3..890ad049190ce 100644 --- a/apps/studio/data/profile/profile-audit-logs-query.ts +++ b/apps/studio/data/profile/profile-audit-logs-query.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query' import { profileKeys } from './keys' import { get, handleError } from '@/data/fetchers' -import { V2AuditLog } from '@/data/organizations/organization-audit-logs-query' +import { AuditLog } from '@/data/organizations/organization-audit-logs-query' import { IS_PLATFORM } from '@/lib/constants' import type { ResponseError, UseCustomQueryOptions } from '@/types' @@ -20,7 +20,6 @@ export async function getProfileAuditLogs( query: { iso_timestamp_start, iso_timestamp_end, - format: 'v2', }, }, signal, @@ -32,7 +31,7 @@ export async function getProfileAuditLogs( export type ProfileAuditLogsError = ResponseError export type ProfileAuditLogsData = { - result: V2AuditLog[] + result: AuditLog[] retention_period: number } diff --git a/apps/studio/tests/components/AuditLogs/AuditLogs.utils.test.ts b/apps/studio/tests/components/AuditLogs/AuditLogs.utils.test.ts index 7ea97248939c3..b68bb5c1b0123 100644 --- a/apps/studio/tests/components/AuditLogs/AuditLogs.utils.test.ts +++ b/apps/studio/tests/components/AuditLogs/AuditLogs.utils.test.ts @@ -8,7 +8,7 @@ import { } from '@/components/interfaces/Organization/AuditLogs/AuditLogs.utils' import { TIMESTAMP_MICROS_PER_MS, - type V2AuditLog, + type AuditLog, } from '@/data/organizations/organization-audit-logs-query' // Timestamps are in microseconds (e.g. 1777471903844000 = April 2026) @@ -16,7 +16,7 @@ const TS_A = 1777471903844000 const TS_B = 1777471903845000 const TS_C = 1777471903846000 -function makeLog(overrides: Partial = {}): V2AuditLog { +function makeLog(overrides: Partial = {}): AuditLog { return { timestamp: TS_A, request_id: 'req-1', From 77140cae32e52845b4d1d798e5f2a7b5b69da6dc Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 5 May 2026 02:30:41 +0200 Subject: [PATCH 2/5] fix: table hover styles are incorrect (#45512) ## Problem When migrating to tailwind v4, we introduced a regression on table styles when hovering a row: image ## Solution Fix the styles: image ## Summary by CodeRabbit * **Style** * Enhanced table row and cell styling to improve hover effects and selection state visual feedback, providing clearer and more consistent interactions when working with tabular data. --- packages/ui/src/components/shadcn/ui/table.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/shadcn/ui/table.tsx b/packages/ui/src/components/shadcn/ui/table.tsx index ae60bcbcd5d60..d4841181f8762 100644 --- a/packages/ui/src/components/shadcn/ui/table.tsx +++ b/packages/ui/src/components/shadcn/ui/table.tsx @@ -52,10 +52,7 @@ const TableRow = React.forwardRef ( td]:hover:bg-surface-200 data-[state=selected]:bg-muted', - className - )} + className={cn('border-b group data-[state=selected]:bg-muted', className)} {...props} /> ) @@ -151,7 +148,10 @@ const TableCell = React.forwardRef< >(({ className, ...props }, ref) => ( )) From 22e3ffc246ff41b13ebd1741aef6319ad7aa6ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Pozo?= Date: Mon, 4 May 2026 20:55:52 -0500 Subject: [PATCH 3/5] docs: fix anchor duplication (#45548) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Small bug on https://supabase.com/docs/guides/functions/auth that shows 2 anchors with the same heading active. ## What is the current behavior? Currently we show case 2 different implementations (raw and with server sdk) in separate sections. Intentionally we want to show the same heading under each section so is a 1-1 comparison. The issue is that anchor links on the second section always point to the first section, and on the navigation bar, both show as active. ## What is the new behavior? Fix headings with proper custom anchors. ## Summary by CodeRabbit * **Documentation** * Enhanced authentication guide documentation with improved section navigation anchors for better cross-reference linking and accessibility within guides. --- apps/docs/content/guides/functions/auth.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/content/guides/functions/auth.mdx b/apps/docs/content/guides/functions/auth.mdx index 9ad4e83383b3d..746a337f1d0e7 100644 --- a/apps/docs/content/guides/functions/auth.mdx +++ b/apps/docs/content/guides/functions/auth.mdx @@ -171,7 +171,7 @@ See the [`@supabase/server` docs](https://github.com/supabase/server) for the fu -### Authenticated user calls +### Authenticated user calls [#authenticated-user-calls-with-server-sdk] `allow: 'user'` pairs with `verify_jwt = true`. The platform validates the JWT, and the SDK hands you `ctx.supabase` already scoped to the caller. @@ -186,7 +186,7 @@ export default { } ``` -### Service-to-service calls +### Service-to-service calls [#service-to-service-calls-with-server-sdk] `allow: 'secret:'` validates the `apikey` header against the named secret key from your [dashboard](/dashboard/project/_/settings/api-keys) and gives you `ctx.supabaseAdmin` for privileged work. The `` matches the name you gave the key. Keep `verify_jwt = false`. @@ -209,11 +209,11 @@ Create a named secret key for each caller in the [**Settings > API keys**](/dash -### Public functions +### Public functions [#public-functions-with-server-sdk] The SDK adds nothing to a truly public function. Use the raw pattern from the previous section. If you need a Supabase client anyway, `allow: 'always'` with `verify_jwt = false` skips every check and treats every caller as anonymous. -### External webhooks +### External webhooks [#external-webhooks-with-server-sdk] Use `allow: 'always'` to skip the SDK's credential check, then verify the provider's signature inside the handler. Keep `verify_jwt = false`. From aab3924eef8a22d6d75f3067c4f2c5a7c6dcdbb7 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 5 May 2026 10:54:18 +0800 Subject: [PATCH 4/5] Refactor Invoice estimate tooltip in plan update confirmation dialog (#45446) ## Context Main fix is to ensure that the tooltip here is scrollable - but also adding some refactors This is the org billing page when downgrading an org ### Before image ### After image ## Changes involved - Use HoverCard for invoice estimate in plan confirmation dialog - Also nudge the UI a little, e.g use a separate column for the compute prices + adjust text color to improve clarity - Refactor usage of `any` for some of the TS declarations ## Summary by CodeRabbit * **New Features** * Added an invoice estimate tooltip in subscription settings showing monthly charges with plan fees, combined compute rows, per-project compute costs, optional compute credits, and a total monthly estimate. * **Refactor** * Simplified the plan update flow by consolidating subscription preview handling and extracting the invoice UI into the new tooltip component. * **Chores** * Improved internal type definitions for subscription preview data and pricing tier identifiers. --- .../Subscription/InvoiceEstimateTooltip.tsx | 231 +++++++++++++++++ .../Subscription/PlanUpdateSidePanel.tsx | 36 +-- .../SubscriptionPlanUpdateDialog.tsx | 243 ++---------------- ...ganization-billing-subscription-preview.ts | 7 +- packages/shared-data/plans.ts | 2 +- 5 files changed, 279 insertions(+), 240 deletions(-) create mode 100644 apps/studio/components/interfaces/Organization/BillingSettings/Subscription/InvoiceEstimateTooltip.tsx diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/InvoiceEstimateTooltip.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/InvoiceEstimateTooltip.tsx new file mode 100644 index 0000000000000..e710b2922d1c2 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/InvoiceEstimateTooltip.tsx @@ -0,0 +1,231 @@ +import { HelpCircle } from 'lucide-react' +import Link from 'next/link' +import { + cn, + HoverCard, + HoverCardContent, + HoverCardTrigger, + Table, + TableBody, + TableCell, + TableRow, +} from 'ui' +import { InfoTooltip } from 'ui-patterns/info-tooltip' +import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' + +import AlertError from '@/components/ui/AlertError' +import { type OrganizationBillingSubscriptionPreviewQueryResult } from '@/data/organizations/organization-billing-subscription-preview' +import { DOCS_URL } from '@/lib/constants' +import { formatCurrency } from '@/lib/helpers' + +const CELL_CLASSNAME = 'py-2 px-0' + +interface InvoiceEstimateTooltipProps { + subscriptionPreviewQueryResult: OrganizationBillingSubscriptionPreviewQueryResult +} + +export const InvoiceEstimateTooltip = ({ + subscriptionPreviewQueryResult, +}: InvoiceEstimateTooltipProps) => { + const { + data: subscriptionPreview, + error: subscriptionPreviewError, + isPending: subscriptionPreviewIsLoading, + isSuccess: subscriptionPreviewInitialized, + } = subscriptionPreviewQueryResult + + return ( + + + + + +

Your new monthly invoice

+

+ First project included. Additional projects cost $10+/month + regardless of activity.{' '} + + Learn more + + . +

+ + {subscriptionPreviewError && ( + + )} + + {subscriptionPreviewIsLoading && ( +
+ Estimating monthly costs... + +
+ )} + + {subscriptionPreviewInitialized && ( +
+ + + {/* Non-compute items and Projects list */} + {(() => { + // Combine all compute-related projects + const computeItems = + subscriptionPreview?.breakdown?.filter( + (item) => + item.description?.toLowerCase().includes('compute') && + item.breakdown && + item.breakdown.length > 0 + ) || [] + + const computeCreditsItem = + subscriptionPreview?.breakdown?.find((item) => + item.description?.startsWith('Compute Credits') + ) ?? null + + const planItem = subscriptionPreview?.breakdown?.find((item) => + item.description?.toLowerCase().includes('plan') + ) + + const allProjects = computeItems.flatMap((item) => + (item.breakdown || []).map((project) => ({ + ...project, + computeType: item.description, + computeCosts: Math.round(item.total_price / item.breakdown!.length), + })) + ) + + const otherItems = + subscriptionPreview?.breakdown?.filter( + (item) => + !item.description?.toLowerCase().includes('compute') && + !item.description?.toLowerCase().includes('plan') + ) || [] + + const content = ( + <> + {planItem && ( + + {planItem.description} + + {formatCurrency(planItem.total_price)} + + + )} + + {/* Combined projects section */} + {allProjects.length > 0 && ( + <> + + + Compute + + + {formatCurrency( + computeItems.reduce( + (sum: number, item) => sum + item.total_price, + 0 + ) + (computeCreditsItem?.total_price ?? 0) + )} + + + + {allProjects.map((project) => ( + + +

+ {project.project_name} ({project.computeType}) +

+
+ + {formatCurrency(project.computeCosts)} + +
+ ))} + + {computeCreditsItem && ( + + + Compute Credits + + + {formatCurrency(computeCreditsItem.total_price)} + + + )} + + )} + + {/* Non-compute items */} + {otherItems.map((item) => ( + + +
+ {item.description ?? 'Unknown'} + {item.breakdown && item.breakdown.length > 0 && ( + +

Projects using {item.description}:

+
    + {item.breakdown.map((breakdown) => ( +
  • + {breakdown.project_name} +
  • + ))} +
+
+ )} +
+
+ + {formatCurrency(item.total_price)} + +
+ ))} + + ) + return content + })()} + + + + Total per month (excluding other usage) + + + {formatCurrency( + subscriptionPreview?.breakdown?.reduce( + (prev, cur) => prev + cur.total_price, + 0 + ) ?? 0 + )} + + +
+
+
+ )} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index a527b6153bbe8..9babe1cb8dd19 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -135,13 +135,7 @@ export const PlanUpdateSidePanel = () => { { enabled: visible } ) - const { - data: subscriptionPreview, - error: subscriptionPreviewError, - isPending: subscriptionPreviewIsLoading, - isFetching: subscriptionPreviewIsFetching, - isSuccess: subscriptionPreviewInitialized, - } = useOrganizationBillingSubscriptionPreview({ + const subscriptionPreviewData = useOrganizationBillingSubscriptionPreview({ tier: selectedTier, organizationSlug: slug, address: debouncedAddress, @@ -181,10 +175,10 @@ export const PlanUpdateSidePanel = () => { }, [visible]) useEffect(() => { - if (visible && isSuccessSubscription) { + if (visible && isSuccessSubscription && subscription.plan.id) { originalPlanRef.current = subscription.plan.id } - }, [visible, isSuccessSubscription]) + }, [visible, isSuccessSubscription, subscription?.plan.id]) const onConfirmDowngrade = () => { setSelectedTier(undefined) @@ -198,6 +192,14 @@ export const PlanUpdateSidePanel = () => { const planMeta = selectedTier ? availablePlans.find((p) => p.id === selectedTier.split('tier_')[1]) : null + + const currentPlanMeta = { + ...availablePlans.find((p) => p.id === subscription?.plan?.id), + features: + subscriptionsPlans.find((plan) => plan.id === `tier_${subscription?.plan?.id}`)?.features || + [], + } + const stripeProjectsUpgradeCommand = getStripeProjectsUpgradeCommand( selectedOrganization?.plan?.id ?? subscription?.plan?.id ) @@ -321,7 +323,7 @@ export const PlanUpdateSidePanel = () => { hasOrioleProjects } onClick={() => { - setSelectedTier(plan.id as any) + setSelectedTier(plan.id as 'tier_free' | 'tier_pro' | 'tier_team') sendEvent({ action: 'studio_pricing_plan_cta_clicked', properties: { @@ -403,19 +405,9 @@ export const PlanUpdateSidePanel = () => { selectedTier={selectedTier} onClose={() => setSelectedTier(undefined)} planMeta={planMeta} - subscriptionPreviewError={subscriptionPreviewError} - subscriptionPreviewIsLoading={subscriptionPreviewIsLoading} - subscriptionPreviewIsFetching={subscriptionPreviewIsFetching} - subscriptionPreviewInitialized={subscriptionPreviewInitialized} - subscriptionPreview={subscriptionPreview} - subscription={subscription} + subscriptionPreviewQueryResult={subscriptionPreviewData} projects={orgProjects} - currentPlanMeta={{ - ...availablePlans.find((p) => p.id === subscription?.plan?.id), - features: - subscriptionsPlans.find((plan) => plan.id === `tier_${subscription?.plan?.id}`) - ?.features || [], - }} + currentPlanMeta={currentPlanMeta} onAddressChange={handleAddressChange} onTaxIdChange={handleTaxIdChange} useAsDefaultBillingAddress={useAsDefaultBillingAddress} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx index 687bb4cecd377..fe86ce8138786 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx @@ -1,16 +1,18 @@ import { Elements } from '@stripe/react-stripe-js' import { loadStripe, PaymentIntentResult, StripeElementsOptions } from '@stripe/stripe-js' +import { useParams } from 'common' import { Check, InfoIcon } from 'lucide-react' import { useTheme } from 'next-themes' import Link from 'next/link' import { useMemo, useRef, useState } from 'react' import { plans as subscriptionsPlans } from 'shared-data/plans' import { toast } from 'sonner' -import { Button, cn, Dialog, DialogContent, Table, TableBody, TableCell, TableRow } from 'ui' +import { Button, cn, Dialog, DialogContent } from 'ui' import { Admonition } from 'ui-patterns' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' +import { InvoiceEstimateTooltip } from './InvoiceEstimateTooltip' import PaymentMethodSelection from './PaymentMethodSelection' import { getStripeElementsAppearanceOptions } from '@/components/interfaces/Billing/Payment/Payment.utils' import { PaymentConfirmation } from '@/components/interfaces/Billing/Payment/PaymentConfirmation' @@ -19,13 +21,13 @@ import { billingPartnerLabel, getPlanChangeType, } from '@/components/interfaces/Billing/Subscription/Subscription.utils' -import AlertError from '@/components/ui/AlertError' -import { OrganizationBillingSubscriptionPreviewData } from '@/data/organizations/organization-billing-subscription-preview' +import { type OrganizationBillingSubscriptionPreviewQueryResult } from '@/data/organizations/organization-billing-subscription-preview' import type { CustomerAddress, CustomerTaxId } from '@/data/organizations/types' import { OrgProject } from '@/data/projects/org-projects-infinite-query' import { useConfirmPendingSubscriptionChangeMutation } from '@/data/subscriptions/org-subscription-confirm-pending-change' +import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' import { useOrgSubscriptionUpdateMutation } from '@/data/subscriptions/org-subscription-update-mutation' -import { SubscriptionTier } from '@/data/subscriptions/types' +import { OrgPlan, SubscriptionTier } from '@/data/subscriptions/types' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { DOCS_URL, @@ -62,14 +64,9 @@ type BreakdownItem = interface Props { selectedTier: 'tier_free' | 'tier_pro' | 'tier_team' | undefined onClose: () => void - planMeta: any - subscriptionPreviewError: any - subscriptionPreviewIsLoading: boolean - subscriptionPreviewIsFetching: boolean - subscriptionPreviewInitialized: boolean - subscriptionPreview: OrganizationBillingSubscriptionPreviewData | undefined - subscription: any - currentPlanMeta: any + planMeta?: OrgPlan | null + currentPlanMeta?: Partial & { features: (string | string[])[] } + subscriptionPreviewQueryResult: OrganizationBillingSubscriptionPreviewQueryResult projects: OrgProject[] onAddressChange?: (address: CustomerAddress) => void onTaxIdChange?: (taxId: CustomerTaxId | null) => void @@ -81,12 +78,7 @@ export const SubscriptionPlanUpdateDialog = ({ selectedTier, onClose, planMeta, - subscriptionPreviewError, - subscriptionPreviewIsLoading, - subscriptionPreviewIsFetching, - subscriptionPreviewInitialized, - subscriptionPreview, - subscription, + subscriptionPreviewQueryResult, currentPlanMeta, projects, onAddressChange, @@ -94,6 +86,7 @@ export const SubscriptionPlanUpdateDialog = ({ useAsDefaultBillingAddress, onUseAsDefaultBillingAddressChange, }: Props) => { + const { slug } = useParams() const { resolvedTheme } = useTheme() const { data: selectedOrganization } = useSelectedOrganizationQuery() const [selectedPaymentMethod, setSelectedPaymentMethod] = useState() @@ -104,6 +97,16 @@ export const SubscriptionPlanUpdateDialog = ({ validateBillingProfile: () => Promise }>(null) + const { + data: subscriptionPreview, + isPending: subscriptionPreviewIsLoading, + isFetching: subscriptionPreviewIsFetching, + isSuccess: subscriptionPreviewInitialized, + } = subscriptionPreviewQueryResult + + const { data: subscription } = useOrgSubscriptionQuery({ + orgSlug: slug, + }) const billingViaPartner = subscription?.billing_via_partner === true const billingPartner = subscription?.billing_partner @@ -225,7 +228,7 @@ export const SubscriptionPlanUpdateDialog = ({ // Features that will be lost when downgrading const featuresToLose = changeType === 'downgrade' - ? currentPlanFeatures.filter((feature: string | [string, ...any[]]) => { + ? currentPlanFeatures.filter((feature) => { const featureStr = typeof feature === 'string' ? feature : feature[0] // Check if this feature exists in the new plan return !topFeatures.some((newFeature: string | string[]) => { @@ -367,6 +370,7 @@ export const SubscriptionPlanUpdateDialog = ({

This organization is billed through our partner{' '} {billingPartnerLabel(billingPartner)}.{' '} + {/* @ts-ignore [Joshen] Might be API types issue */} {billingPartner === 'aws' ? ( <>The organization's credit balance will be decreased accordingly. ) : ( @@ -448,202 +452,9 @@ export const SubscriptionPlanUpdateDialog = ({

Monthly invoice estimate - -
-

Your new monthly invoice

-

- First project included. Additional projects cost{' '} - $10+/month regardless of activity.{' '} - - Learn more - - . -

- - {subscriptionPreviewError && ( - - )} - - {subscriptionPreviewIsLoading && ( -
- Estimating monthly costs... - - - -
- )} - - {subscriptionPreviewInitialized && ( - <> - - - {/* Non-compute items and Projects list */} - {(() => { - // Combine all compute-related projects - const computeItems = - subscriptionPreview?.breakdown?.filter( - (item) => - item.description?.toLowerCase().includes('compute') && - item.breakdown && - item.breakdown.length > 0 - ) || [] - - const computeCreditsItem = - subscriptionPreview?.breakdown?.find((item) => - item.description.startsWith('Compute Credits') - ) ?? null - - const planItem = subscriptionPreview?.breakdown?.find( - (item) => item.description?.toLowerCase().includes('plan') - ) - - const allProjects = computeItems.flatMap((item) => - (item.breakdown || []).map((project) => ({ - ...project, - computeType: item.description, - computeCosts: Math.round( - item.total_price / item.breakdown!.length - ), - })) - ) - - const otherItems = - subscriptionPreview?.breakdown?.filter( - (item) => - !item.description?.toLowerCase().includes('compute') && - !item.description?.toLowerCase().includes('plan') - ) || [] - - const content = ( - <> - {planItem && ( - - - {planItem.description} - - - {formatCurrency(planItem.total_price)} - - - )} - - {/* Combined projects section */} - {allProjects.length > 0 && ( - <> - - - Compute - - - {formatCurrency( - computeItems.reduce( - (sum: number, item) => sum + item.total_price, - 0 - ) + (computeCreditsItem?.total_price ?? 0) - )} - - - {/* Show first 3 projects */} - {allProjects.map((project) => ( - - - {project.project_name} ({project.computeType}) |{' '} - {formatCurrency(project.computeCosts)} - - - ))} - {computeCreditsItem && ( - - - Compute Credits |{' '} - {formatCurrency(computeCreditsItem.total_price)} - - - )} - - )} - - {/* Non-compute items */} - {otherItems.map((item) => ( - - -
- {item.description ?? 'Unknown'} - {item.breakdown && item.breakdown.length > 0 && ( - -

Projects using {item.description}:

-
    - {item.breakdown.map((breakdown) => ( -
  • - {breakdown.project_name} -
  • - ))} -
-
- )} -
-
- - {formatCurrency(item.total_price)} - -
- ))} - - ) - return content - })()} - - - - Total per month (excluding other usage) - - - {formatCurrency( - subscriptionPreview?.breakdown?.reduce( - (prev, cur) => prev + cur.total_price, - 0 - ) ?? 0 - )} - - -
-
- - )} -
-
+
{formatCurrency( @@ -730,7 +541,7 @@ export const SubscriptionPlanUpdateDialog = ({ Please review carefully before downgrading.

- {featuresToLose.map((feature: string | [string, ...any[]]) => ( + {featuresToLose.map((feature) => (
> +export type OrganizationBillingSubscriptionPreviewQueryResult = UseQueryResult< + OrganizationBillingSubscriptionPreviewData, + ResponseError +> + export const useOrganizationBillingSubscriptionPreview = < TData = OrganizationBillingSubscriptionPreviewData, >( diff --git a/packages/shared-data/plans.ts b/packages/shared-data/plans.ts index 646913bf9806a..55608fc718e1f 100644 --- a/packages/shared-data/plans.ts +++ b/packages/shared-data/plans.ts @@ -1,7 +1,7 @@ export type PlanId = 'free' | 'pro' | 'team' | 'enterprise' export interface PricingInformation { - id: string + id: 'tier_free' | 'tier_pro' | 'tier_team' | 'tier_enterprise' planId: PlanId name: string nameBadge?: string From 89760b26e071d7a7b3319fb9fb5867a4609d6c14 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 5 May 2026 11:46:49 +0800 Subject: [PATCH 5/5] Refactor database publications to use page layout component (#45456) ## Context Just refactors Database publications pages to use the `PageLayout` component, otherwise was missing a header currently Also fix search results empty state for publications pages ### Before image ### After image ## Summary by CodeRabbit * **New Features** * Added a dedicated shimmering skeleton for publications table loading states * **Refactor** * Restructured Publications interface for unified table rendering * Unified loading, error, empty and "missing selection" states into the table * Moved empty-results to render inside the table * Removed the back-navigation button * Page layout and section structure refactored for clearer spacing and navigation * **Style** * Improved loading visuals with skeleton rows * Updated empty-results styling for a cleaner table appearance --- .../Publications/PublicationSkeleton.tsx | 18 ++ .../Publications/PublicationsList.tsx | 60 +++---- .../Publications/PublicationsTables.tsx | 161 ++++++++++-------- .../[ref]/database/publications/[id].tsx | 41 ++++- .../[ref]/database/publications/index.tsx | 30 +--- 5 files changed, 178 insertions(+), 132 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx index 853e575b6a5bc..ed318a5a11f42 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationSkeleton.tsx @@ -27,3 +27,21 @@ export const PublicationSkeleton = ({ index }: PublicationSkeletonProps) => { ) } + +export const PublicationTablesSkeleton = ({ index }: PublicationSkeletonProps) => { + return ( + + + + + + + + +
+ +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx index f7e2820a33590..bf9adf22e6c0e 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx @@ -112,29 +112,27 @@ export const PublicationsList = () => { return ( <> -
-
-
- } - className="w-48" - placeholder="Search for a publication" - value={filterString} - onChange={(e) => setFilterString(e.target.value)} - onKeyDown={onSearchInputEscape(filterString, setFilterString)} +
+
+ } + className="w-48" + placeholder="Search for a publication" + value={filterString} + onChange={(e) => setFilterString(e.target.value)} + onKeyDown={onSearchInputEscape(filterString, setFilterString)} + /> +
+ {isPermissionsLoaded && !canUpdatePublications && ( +
+ } + title="You need additional permissions to update database publications" />
- {isPermissionsLoaded && !canUpdatePublications && ( -
- } - title="You need additional permissions to update database publications" - /> -
- )} -
+ )}
@@ -163,6 +161,18 @@ export const PublicationsList = () => { )} + {!isLoading && publications.length === 0 && ( + + + setFilterString('')} + className="border-none !p-0" + /> + + + )} + {isSuccess && publications.map((x) => ( @@ -223,14 +233,6 @@ export const PublicationsList = () => {
- {!isLoading && publications.length === 0 && ( - setFilterString('')} - className="rounded-t-none border-t-0" - /> - )} - { - const { ref, id } = useParams() + const { id } = useParams() const { data: project } = useSelectedProjectQuery() const [filterString, setFilterString] = useState('') + const searchInputRef = useRef(null) const { can: canUpdatePublications, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'publications' ) + useShortcut( + SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH, + () => { + searchInputRef.current?.focus() + searchInputRef.current?.select() + }, + { label: 'Search publications' } + ) + const { data: publications = [] } = useDatabasePublicationsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, @@ -53,27 +65,16 @@ export const PublicationsTables = () => { <>
-
- } - style={{ padding: '5px' }} - tooltip={{ content: { side: 'bottom', text: 'Go back to publications list' } }} - > - - -
- setFilterString(e.target.value)} - icon={} - className="w-48" - /> -
-
+ } + className="w-48" + placeholder="Search for a table" + value={filterString} + onChange={(e) => setFilterString(e.target.value)} + onKeyDown={onSearchInputEscape(filterString, setFilterString)} + /> {!isLoadingPermissions && !canUpdatePublications && ( {
- {(isLoading || isLoadingPermissions) && ( -
- -
- )} + + + + + Name + Schema + Description + {/* + We've disabled All tables toggle for publications. + See https://github.com/supabase/supabase/pull/7233. + */} + + + + + {(isLoading || isLoadingPermissions) && + Array.from({ length: 2 }).map((_, i) => ( + + ))} + + {isError && ( + + + + + + )} - {isError && } + {!isLoading && !isLoadingPermissions && tables.length === 0 && ( + + + setFilterString('')} + /> + + + )} - {isSuccess && - (tables.length === 0 ? ( - setFilterString('')} /> - ) : ( - -
- + {isSuccess ? ( + !!selectedPublication ? ( + tables.map((table) => ( + + )) + ) : ( - Name - Schema - Description - {/* - We've disabled All tables toggle for publications. - See https://github.com/supabase/supabase/pull/7233. - */} - + +

The selected publication with ID {id} cannot be found

+

+ Head back to the list of publications to select one from there +

+
-
- - {!!selectedPublication ? ( - tables.map((table) => ( - - )) - ) : ( - - -

The selected publication with ID {id} cannot be found

-

- Head back to the list of publications to select one from there -

-
-
- )} -
-
-
- ))} + ) + ) : null} + + + ) } diff --git a/apps/studio/pages/project/[ref]/database/publications/[id].tsx b/apps/studio/pages/project/[ref]/database/publications/[id].tsx index 13c9134f62f22..26ba821eea1ad 100644 --- a/apps/studio/pages/project/[ref]/database/publications/[id].tsx +++ b/apps/studio/pages/project/[ref]/database/publications/[id].tsx @@ -1,29 +1,54 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useParams } from 'common' +import { PageContainer, PageSection, PageSectionContent, ShimmeringLoader } from 'ui-patterns' import { PublicationsTables } from '@/components/interfaces/Database/Publications/PublicationsTables' import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout' -import DefaultLayout from '@/components/layouts/DefaultLayout' -import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' -import NoPermission from '@/components/ui/NoPermission' +import { DefaultLayout } from '@/components/layouts/DefaultLayout' +import { PageLayout } from '@/components/layouts/PageLayout/PageLayout' +import { NoPermission } from '@/components/ui/NoPermission' +import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import type { NextPageWithLayout } from '@/types' const DatabasePublications: NextPageWithLayout = () => { + const { ref, id } = useParams() + const { data: project } = useSelectedProjectQuery() const { can: canViewPublications, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_READ, 'publications' ) + const { data: publications = [], isPending } = useDatabasePublicationsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const selectedPublication = publications.find((pub) => pub.id === Number(id)) + if (isPermissionsLoaded && !canViewPublications) { return } return ( - - - - - + : (selectedPublication?.name ?? '')} + breadcrumbs={[ + { + label: 'Publications', + href: `/project/${ref}/database/publications`, + }, + ]} + size="large" + > + + + + + + + + ) } diff --git a/apps/studio/pages/project/[ref]/database/publications/index.tsx b/apps/studio/pages/project/[ref]/database/publications/index.tsx index 5fc74efeccd0d..ded564a278aed 100644 --- a/apps/studio/pages/project/[ref]/database/publications/index.tsx +++ b/apps/studio/pages/project/[ref]/database/publications/index.tsx @@ -1,17 +1,12 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { PageContainer } from 'ui-patterns/PageContainer' -import { - PageHeader, - PageHeaderMeta, - PageHeaderSummary, - PageHeaderTitle, -} from 'ui-patterns/PageHeader' -import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' +import { PageSection } from 'ui-patterns/PageSection' import { PublicationsList } from '@/components/interfaces/Database/Publications/PublicationsList' import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout' -import DefaultLayout from '@/components/layouts/DefaultLayout' -import NoPermission from '@/components/ui/NoPermission' +import { DefaultLayout } from '@/components/layouts/DefaultLayout' +import { PageLayout } from '@/components/layouts/PageLayout/PageLayout' +import { NoPermission } from '@/components/ui/NoPermission' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from '@/types' @@ -26,22 +21,13 @@ const DatabasePublications: NextPageWithLayout = () => { } return ( - <> - - - - Database Publications - - - + - - - - + + - + ) }