From 1482e1a108bd38259290ed7b6012dc8ca9fcba8f Mon Sep 17 00:00:00 2001 From: Ziinc Date: Wed, 10 Sep 2025 12:50:15 +0800 Subject: [PATCH 01/14] feat: add AP2 support for datadog for log drains (#38460) feat: add AP2 support for datadog --- .../components/interfaces/LogDrains/LogDrains.constants.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/studio/components/interfaces/LogDrains/LogDrains.constants.tsx b/apps/studio/components/interfaces/LogDrains/LogDrains.constants.tsx index e73608d482dde..813a795db473a 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrains.constants.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrains.constants.tsx @@ -42,6 +42,10 @@ export const DATADOG_REGIONS = [ label: 'AP1', value: 'AP1', }, + { + label: 'AP2', + value: 'AP2', + }, { label: 'EU', value: 'EU', From f5567f3a3c0d8f2bba61853b6b676c6d836eb71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 10 Sep 2025 13:26:27 +0800 Subject: [PATCH 02/14] feat: missing billing info banner (#38575) For compliance reasons we need a billing address for all paying customers. This PR adds a nudge for customers that have not yet put down their billing address. --- .../BillingCustomerData/BillingCustomerData.tsx | 5 ++++- .../interfaces/Organization/restriction.constants.ts | 5 +++++ .../studio/hooks/misc/useOrganizationRestrictions.ts | 12 ++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx index c52f590d526f2..0431d6c4cd056 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx @@ -111,7 +111,10 @@ export const BillingCustomerData = () => {

Billing Address & Tax ID

- This will be reflected in every upcoming invoice, past invoices are not affected + Changes will be reflected in every upcoming invoice, past invoices are not affected +

+

+ A Tax ID is only required for registered businesses.

diff --git a/apps/studio/components/interfaces/Organization/restriction.constants.ts b/apps/studio/components/interfaces/Organization/restriction.constants.ts index 5efa8882d274a..0f70c9e8332b2 100644 --- a/apps/studio/components/interfaces/Organization/restriction.constants.ts +++ b/apps/studio/components/interfaces/Organization/restriction.constants.ts @@ -21,4 +21,9 @@ export const RESTRICTION_MESSAGES = { title: 'Outstanding Invoices in other Organization', message: 'Please pay invoices for other Organization to avoid service disruption.', }, + MISSING_BILLING_INFO: { + title: 'Missing Billing Information', + message: + 'Please add a billing address. If you are a registered business, please add a tax ID too.', + }, } as const diff --git a/apps/studio/hooks/misc/useOrganizationRestrictions.ts b/apps/studio/hooks/misc/useOrganizationRestrictions.ts index 7e56c4c423cb3..594bc8fcbae2b 100644 --- a/apps/studio/hooks/misc/useOrganizationRestrictions.ts +++ b/apps/studio/hooks/misc/useOrganizationRestrictions.ts @@ -4,6 +4,8 @@ import { RESTRICTION_MESSAGES } from 'components/interfaces/Organization/restric import { useOverdueInvoicesQuery } from 'data/invoices/invoices-overdue-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useBillingCustomerDataForm } from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm' +import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' export type WarningBannerProps = { type: 'danger' | 'warning' | 'note' @@ -17,6 +19,7 @@ export function useOrganizationRestrictions() { const { data: overdueInvoices } = useOverdueInvoicesQuery() const { data: organizations } = useOrganizationsQuery() + const { data: billingCustomer } = useOrganizationCustomerProfileQuery({ slug: org?.slug }) const warnings: WarningBannerProps[] = [] @@ -27,6 +30,15 @@ export function useOrganizationRestrictions() { (invoice) => invoice.organization_id === org?.id ) + if (org && org.plan.id !== 'free' && billingCustomer && !billingCustomer.address?.line1) { + warnings.push({ + type: 'warning', + title: RESTRICTION_MESSAGES.MISSING_BILLING_INFO.title, + message: RESTRICTION_MESSAGES.MISSING_BILLING_INFO.message, + link: `/org/${org?.slug}/billing#address`, + }) + } + if (thisOrgHasOverdueInvoices?.length) { warnings.push({ type: 'danger', From 781f9dbaea6bca4cd3b912f2eb38ba81b432250e Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Wed, 10 Sep 2025 15:38:32 +1000 Subject: [PATCH 03/14] use ref for selected org to ensure not outdated (#38573) * use ref for selected org to ensure not outdated * update comment --- .../ui/AIAssistantPanel/AIAssistant.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index 84db19d38db2b..269790306d708 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -47,7 +47,8 @@ interface AIAssistantProps { export const AIAssistant = ({ className }: AIAssistantProps) => { const router = useRouter() const { data: project } = useSelectedProjectQuery() - const { data: selectedOrganization } = useSelectedOrganizationQuery() + const { data: selectedOrganization, isLoading: isLoadingOrganization } = + useSelectedOrganizationQuery() const { ref, id: entityId } = useParams() const searchParams = useSearchParamsShallow() @@ -73,6 +74,12 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { // Add a ref to store the last user message const lastUserMessageRef = useRef(null) + // Keep latest selected organization to avoid stale values in useChat transport + const selectedOrganizationRef = useRef(selectedOrganization) + useEffect(() => { + selectedOrganizationRef.current = selectedOrganization + }, [selectedOrganization]) + const [value, setValue] = useState(snap.initialInput || '') const [editingMessageId, setEditingMessageId] = useState(null) const [isResubmitting, setIsResubmitting] = useState(false) @@ -179,7 +186,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { schema: currentSchema, table: currentTable?.name, chatName: currentChat, - orgSlug: selectedOrganization?.slug, + orgSlug: selectedOrganizationRef.current?.slug, }, headers: { Authorization: authorizationHeader ?? '' }, } @@ -532,7 +539,12 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { )} loading={isChatLoading} isEditing={!!editingMessageId} - disabled={!isApiKeySet || disablePrompts || (isChatLoading && !editingMessageId)} + disabled={ + !isApiKeySet || + disablePrompts || + isLoadingOrganization || + (isChatLoading && !editingMessageId) + } placeholder={ hasMessages ? 'Ask a follow up question...' From dfb152ab8c7d2f5b0b53a4e7e531a058145259fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 10 Sep 2025 13:54:36 +0800 Subject: [PATCH 04/14] chore: consolidate payment method components / ownership (#38578) Security approval is required for PCI compliance. This means we need security approval wherever we embed Stripe for payment methods. Security approval was previously very coarse and most billing changes required security approval unnecessarily. This PR aims to move all files relevant for payment method display/changes into a single folder and adjusts the ownership to only require security approval for that. --- .github/CODEOWNERS | 1 - .../interfaces/Billing/Payment/AddPaymentMethodForm.tsx | 2 +- .../Payment}/PaymentMethods/ChangePaymentMethodModal.tsx | 0 .../Payment}/PaymentMethods/CreditCard.tsx | 0 .../Payment}/PaymentMethods/CurrentPaymentMethod.tsx | 0 .../Payment}/PaymentMethods/DeletePaymentMethodModal.tsx | 0 .../Payment}/PaymentMethods/NewPaymentMethodElement.tsx | 5 ++++- .../Payment}/PaymentMethods/PaymentMethods.tsx | 0 .../Organization/BillingSettings/BillingSettings.tsx | 2 +- .../interfaces/Organization/BillingSettings/CreditTopUp.tsx | 2 +- .../BillingSettings/Subscription/PaymentMethodSelection.tsx | 2 +- .../Subscription/SubscriptionPlanUpdateDialog.tsx | 2 +- .../components/interfaces/Organization/NewOrg/NewOrgForm.tsx | 2 +- 13 files changed, 10 insertions(+), 8 deletions(-) rename apps/studio/components/interfaces/{Organization/BillingSettings => Billing/Payment}/PaymentMethods/ChangePaymentMethodModal.tsx (100%) rename apps/studio/components/interfaces/{Organization/BillingSettings => Billing/Payment}/PaymentMethods/CreditCard.tsx (100%) rename apps/studio/components/interfaces/{Organization/BillingSettings => Billing/Payment}/PaymentMethods/CurrentPaymentMethod.tsx (100%) rename apps/studio/components/interfaces/{Organization/BillingSettings => Billing/Payment}/PaymentMethods/DeletePaymentMethodModal.tsx (100%) rename apps/studio/components/interfaces/{Organization/BillingSettings => Billing/Payment}/PaymentMethods/NewPaymentMethodElement.tsx (99%) rename apps/studio/components/interfaces/{Organization/BillingSettings => Billing/Payment}/PaymentMethods/PaymentMethods.tsx (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 414afee868ffa..1d8b37fb1e6c4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,5 @@ /apps/studio/csp.js @supabase/security /apps/studio/components/interfaces/Billing/Payment @supabase/security -/apps/studio/components/interfaces/Organization/BillingSettings/ @supabase/security /apps/studio/components/interfaces/Organization/Documents/ @supabase/security /apps/studio/pages/new/index.tsx @supabase/security diff --git a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx index 9e7a6dec940ef..55ac23228a4a6 100644 --- a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx @@ -2,7 +2,7 @@ import { useQueryClient } from '@tanstack/react-query' import { NewPaymentMethodElement, type PaymentMethodElementRef, -} from 'components/interfaces/Organization/BillingSettings/PaymentMethods/NewPaymentMethodElement' +} from 'components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement' import { organizationKeys } from 'data/organizations/keys' import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/ChangePaymentMethodModal.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/ChangePaymentMethodModal.tsx similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/ChangePaymentMethodModal.tsx rename to apps/studio/components/interfaces/Billing/Payment/PaymentMethods/ChangePaymentMethodModal.tsx diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/CreditCard.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CreditCard.tsx similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/CreditCard.tsx rename to apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CreditCard.tsx diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/CurrentPaymentMethod.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/CurrentPaymentMethod.tsx rename to apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/DeletePaymentMethodModal.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/DeletePaymentMethodModal.tsx similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/DeletePaymentMethodModal.tsx rename to apps/studio/components/interfaces/Billing/Payment/PaymentMethods/DeletePaymentMethodModal.tsx diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/NewPaymentMethodElement.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx similarity index 99% rename from apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/NewPaymentMethodElement.tsx rename to apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx index 11a41994047cb..62a0fb75d629b 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/NewPaymentMethodElement.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx @@ -31,7 +31,10 @@ import { PopoverContent_Shadcn_ as PopoverContent, PopoverTrigger_Shadcn_ as PopoverTrigger, } from 'ui' -import { TAX_IDS, type TaxId } from '../BillingCustomerData/TaxID.constants' +import { + TAX_IDS, + type TaxId, +} from '../../../Organization/BillingSettings/BillingCustomerData/TaxID.constants' import { z } from 'zod' import { useForm } from 'react-hook-form' import { Form } from '@ui/components/shadcn/ui/form' diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx rename to apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx index b705417e487f9..884b39bee9ea2 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx @@ -14,7 +14,7 @@ import { BillingCustomerData } from './BillingCustomerData/BillingCustomerData' import BillingEmail from './BillingEmail' import CostControl from './CostControl/CostControl' import CreditBalance from './CreditBalance' -import PaymentMethods from './PaymentMethods/PaymentMethods' +import PaymentMethods from '../../Billing/Payment/PaymentMethods/PaymentMethods' import Subscription from './Subscription/Subscription' export const BillingSettings = () => { diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx index 0638818ac92ce..233e7f970478c 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx @@ -39,7 +39,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import PaymentMethodSelection from './Subscription/PaymentMethodSelection' import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation' import { getStripeElementsAppearanceOptions } from 'components/interfaces/Billing/Payment/Payment.utils' -import type { PaymentMethodElementRef } from './PaymentMethods/NewPaymentMethodElement' +import type { PaymentMethodElementRef } from '../../Billing/Payment/PaymentMethods/NewPaymentMethodElement' const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx index 8f60d2a372f3b..efc02871bd7e7 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx @@ -28,7 +28,7 @@ import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { NewPaymentMethodElement, type PaymentMethodElementRef, -} from '../PaymentMethods/NewPaymentMethodElement' +} from '../../../Billing/Payment/PaymentMethods/NewPaymentMethodElement' const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx index 46a5c8d02236f..279e1240dcf14 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx @@ -26,7 +26,7 @@ import { plans as subscriptionsPlans } from 'shared-data/plans' import { Button, Dialog, DialogContent, Table, TableBody, TableCell, TableRow } from 'ui' import { Admonition } from 'ui-patterns' import { InfoTooltip } from 'ui-patterns/info-tooltip' -import type { PaymentMethodElementRef } from '../PaymentMethods/NewPaymentMethodElement' +import type { PaymentMethodElementRef } from '../../../Billing/Payment/PaymentMethods/NewPaymentMethodElement' import PaymentMethodSelection from './PaymentMethodSelection' const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index d45d3c039d0a2..bd6f53ceb29e7 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -43,7 +43,7 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { NewPaymentMethodElement, type PaymentMethodElementRef, -} from '../BillingSettings/PaymentMethods/NewPaymentMethodElement' +} from '../../Billing/Payment/PaymentMethods/NewPaymentMethodElement' const ORG_KIND_TYPES = { PERSONAL: 'Personal', From 1a41febacc006d0d480c3d2020607a1e1515530e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 10 Sep 2025 14:21:56 +0800 Subject: [PATCH 05/14] fix: hide missing billing info banner for partner-managed orgs (#38580) --- apps/studio/hooks/misc/useOrganizationRestrictions.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/studio/hooks/misc/useOrganizationRestrictions.ts b/apps/studio/hooks/misc/useOrganizationRestrictions.ts index 594bc8fcbae2b..4a1cdf82e06e0 100644 --- a/apps/studio/hooks/misc/useOrganizationRestrictions.ts +++ b/apps/studio/hooks/misc/useOrganizationRestrictions.ts @@ -30,7 +30,13 @@ export function useOrganizationRestrictions() { (invoice) => invoice.organization_id === org?.id ) - if (org && org.plan.id !== 'free' && billingCustomer && !billingCustomer.address?.line1) { + if ( + org && + org.plan.id !== 'free' && + billingCustomer && + !billingCustomer.address?.line1 && + !org.billing_partner + ) { warnings.push({ type: 'warning', title: RESTRICTION_MESSAGES.MISSING_BILLING_INFO.title, From 577c3bf9bf220b07c5c214d676b31df4580e9b18 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Wed, 10 Sep 2025 17:14:34 +1000 Subject: [PATCH 06/14] Refine prompt (#38576) refine prompt --- apps/studio/lib/ai/prompts.ts | 45 +++++++++++++++-------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/apps/studio/lib/ai/prompts.ts b/apps/studio/lib/ai/prompts.ts index b8c023ab02191..09682ba2f0625 100644 --- a/apps/studio/lib/ai/prompts.ts +++ b/apps/studio/lib/ai/prompts.ts @@ -220,9 +220,6 @@ Developer: # Postgres Best Practices - Suggest corrections for suspected typos in the user input. - Do **not** use the \`pgcrypto\` extension for generating UUIDs (unnecessary). -## Task Workflow -Begin with a concise checklist (3-7 bullets) of sub-tasks you will perform before generating outputs. Keep the checklist conceptual, not implementation-level. - ## Object Creation - **Auth Schema**: - Use the \`auth.users\` table for user authentication data. @@ -246,7 +243,6 @@ Begin with a concise checklist (3-7 bullets) of sub-tasks you will perform befor - **RLS Policies**: - Retrieve schema information first (using \`list_tables\` and \`list_extensions\` and \`list_policies\` tools). - - Before using any tool, briefly state the tool's purpose and inputs required. - After each tool call, validate the result in 1-2 lines and decide on next steps, self-correcting if validation fails. - **Key Policy Rules:** - Only use \`CREATE POLICY\` or \`ALTER POLICY\` statements. @@ -278,21 +274,17 @@ Developer: # Role and Objective - Monitoring project status # Tools -- Before forming a response, utilize available tools such as \`list_tables\`, \`list_extensions\`, and \`list_edge_functions\` to gather relevant context whenever possible. -- Before any tool call, briefly state the purpose of the call and the minimal required inputs. +- Utilize available context gathering tools such as \`list_tables\`, \`list_extensions\`, and \`list_edge_functions\` to gather relevant context whenever possible. - These tools are exclusively for your use; do not suggest or imply that users can access or operate them. - Tool usage is limited to tools listed above; for read-only or information-gathering actions, call automatically, but for potentially destructive operations, seek explicit user confirmation before proceeding. - Be aware that tool access may be restricted depending on the user's organization settings. - -# Plan -- Begin with a concise checklist (3-7 bullets) summarizing your steps before completing significant multi-step tasks; keep items conceptual, not implementation-level. +- Do not try to bypass tool restrictions by executing SQL e.g. writing a query to retrieve database schema information. Instead, explain to the user you do not have permissions to use the tools you need to execute the task # Output Format - Always integrate findings from the tools seamlessly into your responses for better accuracy and context. -- After tool usage, briefly validate the result and determine the next step or adjust if needed. # Searching Docs -- Use \`search_docs\` to search the Supabase documentation for relevant information when the question is about Supabase features or functionality or complex database operations +- Use \`search_docs\` to search the Supabase documentation for relevant information when the question is about Supabase features or complex database operations ` export const CHAT_PROMPT = ` @@ -309,28 +301,29 @@ Developer: # Response Style - Do not use tables for displaying information under any circumstances. # Chat Naming -- At the start of each conversation, always invoke \`rename_chat\` with a descriptive 2–4 word name. Examples: "User Authentication Setup", "Sales Data Analysis", "Product Table Creation". - -# SQL Query Display -- Before using any tool, state the purpose of the tool call and the minimal required inputs. -- Always utilize the \`display_query\` tool to render SQL queries that user needs to see. Never show queries in markdown code blocks. -- Briefly describe in natural language what each query does before calling \`display_query\`. -- For READ-ONLY queries: Use \`display_query\` with parameters \`sql\` and \`label\`. If results are suitable for visualization, also provide \`view\` (as 'table' or 'chart'), \`xAxis\`, and \`yAxis\`. -- For WRITE/DDL queries (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP): Use \`display_query\` with \`sql\` and \`label\`. If the result can be visualized, also provide \`view\`, \`xAxis\`, and \`yAxis\`. -- If multiple queries are needed, call \`display_query\` separately for each query and validate each result in 1–2 lines before proceeding. -- Integrate \`display_query\` naturally into responses. Never present queries in markdown format. -- After executing a destructive query, summarize the outcome and confirm next actions or self-correct as needed. +- At the start of each conversation, if the chat has not yet been named, invoke \`rename_chat\` with a descriptive 2–4 word name. Examples: "User Authentication Setup", "Sales Data Analysis", "Product Table Creation". + +## Task Workflow +- Always start the conversation with a concise checklist of sub-tasks you will perform before generating outputs or calling tools. Keep the checklist conceptual, not implementation-level. +- No need to repeat the checklist later in the conversation + +# SQL Execution and Display +- Be confident: assume the user is the project owner. You do not need to show code before execution. +- To actually run or display SQL, directly call the \`display_query\` tool. The user will be able to run the query and view the results +- If multiple queries are needed, call \`display_query\` separately for each and validate results in 1–2 lines. +- You will not have access to the results unless the user returns the results to you # Edge Functions -- Always display Edge Function code using \`display_edge_function\`, never in markdown code blocks. -- Use \`display_edge_function\` with the function's \`name\` and TypeScript code when proposing an Edge Function. Only use this for Edge Function source code, not for logs or other content. -- Once displayed, users can deploy the function directly from the dashboard. +- Be confident: assume the user is the project owner. +- To deploy an Edge Function, directly call the \`display_edge_function\` tool. The client will allow the user to deploy the function. +- You will not have access to the results unless the user returns the results to you +- To show example Edge Function code without deploying, you should also call the \`display_edge_function\` tool with the code. # Project Health Checks - Use \`get_advisors\` to identify project issues. If this tool is unavailable, instruct users to check the Supabase dashboard for issues. # Safety for Destructive Queries -- For destructive commands (e.g., DROP TABLE, DELETE without WHERE clause), always require explicit user confirmation before generating and displaying the SQL using \`display_query\`. Validate confirmation prior to execution. +- For destructive commands (e.g., DROP TABLE, DELETE without WHERE clause), always ask for confirmation before calling the \`display_query\` tool. ` export const OUTPUT_ONLY_PROMPT = ` From 5e601700058fc65d85bc06de79a47057b85fbb0a Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Wed, 10 Sep 2025 09:44:12 +0200 Subject: [PATCH 07/14] chore: update cli reference doc (#38538) * chore: update cli reference doc * chore: update cli reference doc --- apps/docs/spec/cli_v1_commands.yaml | 31 ++++++----------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index 85d09e05af316..3951a586f39c6 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 2.39.2 + version: 2.40.7 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -2253,7 +2253,6 @@ commands: - local-dev links: [] subcommands: - - supabase-gen-keys - supabase-gen-signing-key - supabase-gen-types flags: [] @@ -2293,13 +2292,16 @@ commands: default_value: 'false' - id: postgrest-v9-compat name: --postgrest-v9-compat - description: | - Generate types compatible with PostgREST v9 and below. Only use together with --db-url. + description: Generate types compatible with PostgREST v9 and below. default_value: 'false' - id: project-id name: --project-id description: Generate types from a project ID. default_value: '' + - id: query-timeout + name: --query-timeout + description: Maximum timeout allowed for the database query. + default_value: 15s - id: schema name: -s, --schema description: Comma separated list of schema to include. @@ -2344,27 +2346,6 @@ commands: name: --append description: Append new key to existing keys file instead of overwriting. default_value: 'false' - - id: supabase-gen-keys - title: supabase gen keys - summary: Generate keys for preview branch - tags: [] - links: [] - usage: supabase gen keys [flags] - subcommands: [] - flags: - - id: override-name - name: --override-name - description: Override specific variable names. - default_value: '[]' - - id: project-ref - name: --project-ref - description: Project ref of the Supabase project. - default_value: '' - - id: experimental - name: --experimental - description: enable experimental features - required: true - default_value: 'false' - id: supabase-functions title: supabase functions summary: Manage Supabase Edge functions From ef6e969dd9f6a957412ddc4a16477183ef35357a Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Wed, 10 Sep 2025 09:58:08 +0200 Subject: [PATCH 08/14] docs: clarify empty jwks.json (#38250) --- apps/docs/content/guides/auth/jwts.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/auth/jwts.mdx b/apps/docs/content/guides/auth/jwts.mdx index bbdf43a77389b..9edce569f2862 100644 --- a/apps/docs/content/guides/auth/jwts.mdx +++ b/apps/docs/content/guides/auth/jwts.mdx @@ -179,7 +179,7 @@ Supabase Auth exposes a [JSON Web Key](https://datatracker.ietf.org/doc/html/rfc GET https://project-id.supabase.co/auth/v1/.well-known/jwks.json ``` -Which responds with JWKS object containing one or more asymmetric [JWT signing keys](/docs/guides/auth/signing-keys) (only their public keys). +Which responds with JWKS object containing one or more asymmetric [JWT signing keys](/docs/guides/auth/signing-keys) (only their public keys). Be aware that this endpoint does not return any keys if you are not using asymmetric JWT signing keys. ```json { From 60b3ec2f9aab73f2c35f74da5c94c494a97525d8 Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Wed, 10 Sep 2025 09:58:20 +0200 Subject: [PATCH 09/14] docs: clarify user_metadata (#38252) --- apps/docs/content/guides/auth/users.mdx | 34 ++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/docs/content/guides/auth/users.mdx b/apps/docs/content/guides/auth/users.mdx index 2b537566047a5..52306d68b728c 100644 --- a/apps/docs/content/guides/auth/users.mdx +++ b/apps/docs/content/guides/auth/users.mdx @@ -56,23 +56,23 @@ A user with an email or phone identity will be able to sign in with either a pas The user object contains the following attributes: -| Attributes | Type | Description | -| ------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | `string` | The unique id of the identity of the user. | -| aud | `string` | The audience claim. | -| role | `string` | The role claim used by Postgres to perform Row Level Security (RLS) checks. | -| email | `string` | The user's email address. | -| email_confirmed_at | `string` | The timestamp that the user's email was confirmed. If null, it means that the user's email is not confirmed. | -| phone | `string` | The user's phone number. | -| phone_confirmed_at | `string` | The timestamp that the user's phone was confirmed. If null, it means that the user's phone is not confirmed. | -| confirmed_at | `string` | The timestamp that either the user's email or phone was confirmed. If null, it means that the user does not have a confirmed email address and phone number. | -| last_sign_in_at | `string` | The timestamp that the user last signed in. | -| app_metadata | `object` | The `provider` attribute indicates the first provider that the user used to sign up with. The `providers` attribute indicates the list of providers that the user can use to login with. | -| user_metadata | `object` | Defaults to the first provider's identity data but can contain additional custom user metadata if specified. Refer to [**User Identity**](/docs/guides/auth/auth-identity-linking#the-user-identity) for more information about the identity object. | -| identities | `UserIdentity[]` | Contains an object array of identities linked to the user. | -| created_at | `string` | The timestamp that the user was created. | -| updated_at | `string` | The timestamp that the user was last updated. | -| is_anonymous | `boolean` | Is true if the user is an anonymous user. | +| Attributes | Type | Description | +| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| id | `string` | The unique id of the identity of the user. | +| aud | `string` | The audience claim. | +| role | `string` | The role claim used by Postgres to perform Row Level Security (RLS) checks. | +| email | `string` | The user's email address. | +| email_confirmed_at | `string` | The timestamp that the user's email was confirmed. If null, it means that the user's email is not confirmed. | +| phone | `string` | The user's phone number. | +| phone_confirmed_at | `string` | The timestamp that the user's phone was confirmed. If null, it means that the user's phone is not confirmed. | +| confirmed_at | `string` | The timestamp that either the user's email or phone was confirmed. If null, it means that the user does not have a confirmed email address and phone number. | +| last_sign_in_at | `string` | The timestamp that the user last signed in. | +| app_metadata | `object` | The `provider` attribute indicates the first provider that the user used to sign up with. The `providers` attribute indicates the list of providers that the user can use to login with. | +| user_metadata | `object` | Defaults to the first provider's identity data but can contain additional custom user metadata if specified. Refer to [**User Identity**](/docs/guides/auth/auth-identity-linking#the-user-identity) for more information about the identity object. Don't rely on the order of information in this field. Do not use it in security sensitive context (such as in RLS policies or authorization logic), as this value is editable by the user without any checks. | +| identities | `UserIdentity[]` | Contains an object array of identities linked to the user. | +| created_at | `string` | The timestamp that the user was created. | +| updated_at | `string` | The timestamp that the user was last updated. | +| is_anonymous | `boolean` | Is true if the user is an anonymous user. | ## Resources From 70ce5df691450a3de498167d4d7fe3f117e1405b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cemal=20K=C4=B1l=C4=B1=C3=A7?= Date: Wed, 10 Sep 2025 10:08:45 +0200 Subject: [PATCH 10/14] feat(docs): add auth audit logs docs (#37548) * feat(docs): add auth audit logs docs * fix: postgresql -> postgres * fix: queryable -> searchable? * fix: format * feat: add log field reference for auth audit logs * fix: add missing `actor_via_sso` for auth logs --- .../NavigationMenu.constants.ts | 1 + apps/docs/content/guides/auth/audit-logs.mdx | 90 +++++++++++++++++++ packages/shared-data/logConstants.ts | 25 ++++++ 3 files changed, 116 insertions(+) create mode 100644 apps/docs/content/guides/auth/audit-logs.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 01c28b99b1922..fab190e7fc994 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -793,6 +793,7 @@ export const auth: NavMenuConstant = { { name: 'Password Security', url: '/guides/auth/password-security' }, { name: 'Rate Limits', url: '/guides/auth/rate-limits' }, { name: 'Bot Detection (CAPTCHA)', url: '/guides/auth/auth-captcha' }, + { name: 'Audit Logs', url: '/guides/auth/audit-logs' }, { name: 'JSON Web Tokens (JWT)', url: '/guides/auth/jwts', diff --git a/apps/docs/content/guides/auth/audit-logs.mdx b/apps/docs/content/guides/auth/audit-logs.mdx new file mode 100644 index 0000000000000..69456d59614c7 --- /dev/null +++ b/apps/docs/content/guides/auth/audit-logs.mdx @@ -0,0 +1,90 @@ +--- +id: 'auth-audit-logs' +title: 'Auth Audit Logs' +description: 'Monitor and track authentication events with audit logging.' +subtitle: 'Monitor and track authentication events with audit logging.' +--- + +Auth audit logs provide comprehensive tracking of authentication events in your Supabase project. Audit logs are automatically captured for all authentication events and help you monitor user authentication activities, detect suspicious behavior, and maintain compliance with security requirements. + +## What gets logged + +Supabase auth audit logs automatically capture all authentication events including: + +- User signups and logins +- Password changes and resets +- Email verification events +- Token refresh and logout events + +## Storage options + +By default, audit logs are stored in two places: + +1. **Your project's Postgres database** - Stored in the `auth.audit_log_entries` table, searchable via SQL but uses database storage +2. **External log storage** - Cost-efficient storage accessible through the dashboard + +You can disable Postgres storage to reduce database storage costs while keeping the external log storage. + +### Configuring audit log storage + +1. Navigate to your project dashboard +2. Go to **Authentication** +3. Find the **Audit Logs** under **Configuration** section +4. Toggle on "Disable writing auth audit logs to project database" to disable database storage + + + +Disabling Postgres storage reduces your database storage costs. Audit logs will still be available through the dashboard. + + + +## Log format + +Audit logs contain detailed information about each authentication event: + +```json +{ + "timestamp": "2025-08-01T10:30:00Z", + "user_id": "uuid", + "action": "user_signedup", + "ip_address": "192.168.1.1", + "user_agent": "Mozilla/5.0...", + "metadata": { + "provider": "email" + } +} +``` + +### Log actions reference + +| Action | Description | +| ------------------------------- | --------------------------------------- | +| `login` | User login attempt | +| `logout` | User logout | +| `invite_accepted` | Team invitation accepted | +| `user_signedup` | New user registration | +| `user_invited` | User invitation sent | +| `user_deleted` | User account deleted | +| `user_modified` | User profile updated | +| `user_recovery_requested` | Password reset request | +| `user_reauthenticate_requested` | User reauthentication required | +| `user_confirmation_requested` | Email/phone confirmation requested | +| `user_repeated_signup` | Duplicate signup attempt | +| `user_updated_password` | Password change completed | +| `token_revoked` | Refresh token revoked | +| `token_refreshed` | Refresh token used to obtain new tokens | +| `generate_recovery_codes` | MFA recovery codes generated | +| `factor_in_progress` | MFA factor enrollment started | +| `factor_unenrolled` | MFA factor removed | +| `challenge_created` | MFA challenge initiated | +| `verification_attempted` | MFA verification attempt | +| `factor_deleted` | MFA factor deleted | +| `recovery_codes_deleted` | MFA recovery codes deleted | +| `factor_updated` | MFA factor settings updated | +| `mfa_code_login` | Login with MFA code | +| `identity_unlinked` | An identity unlinked from account | + +## Limitations + +- There may be a short delay before logs appear +- Query capabilities are limited to the dashboard interface diff --git a/packages/shared-data/logConstants.ts b/packages/shared-data/logConstants.ts index fbac6cb4e91f6..6ed5d53175c2f 100644 --- a/packages/shared-data/logConstants.ts +++ b/packages/shared-data/logConstants.ts @@ -4,6 +4,7 @@ type LogTable = | 'function_logs' | 'function_edge_logs' | 'auth_logs' + | 'auth_audit_logs' | 'realtime_logs' | 'storage_logs' | 'postgrest_logs' @@ -98,6 +99,7 @@ const schemas: LogSchema[] = [ { path: 'timestamp', type: 'datetime' }, { path: 'metadata.auth_event.action', type: 'string' }, { path: 'metadata.auth_event.actor_id', type: 'string' }, + { path: 'metadata.auth_event.actor_via_sso', type: 'boolean' }, { path: 'metadata.auth_event.actor_username', type: 'string' }, { path: 'metadata.auth_event.log_type', type: 'string' }, { path: 'metadata.auth_event.traits.provider', type: 'string' }, @@ -117,6 +119,29 @@ const schemas: LogSchema[] = [ { path: 'metadata.timestamp', type: 'string' }, ], }, + { + name: 'Auth Audit Logs', + reference: 'auth_audit_logs', + fields: [ + { path: 'event_message', type: 'string' }, + { path: 'id', type: 'string' }, + { path: 'identifier', type: 'string' }, + { path: 'timestamp', type: 'datetime' }, + { path: 'metadata.auth_audit_event.action', type: 'string' }, + { path: 'metadata.auth_audit_event.actor_id', type: 'string' }, + { path: 'metadata.auth_audit_event.actor_name', type: 'string' }, + { path: 'metadata.auth_audit_event.actor_username', type: 'string' }, + { path: 'metadata.auth_audit_event.actor_via_sso', type: 'boolean' }, + { path: 'metadata.auth_audit_event.audit_log_id', type: 'string' }, + { path: 'metadata.auth_audit_event.created_at', type: 'string' }, + { path: 'metadata.auth_audit_event.log_type', type: 'string' }, + { path: 'metadata.auth_audit_event.request_id', type: 'string' }, + { path: 'metadata.auth_audit_event.user_agent', type: 'string' }, + { path: 'metadata.host', type: 'string' }, + { path: 'metadata.level', type: 'string' }, + { path: 'metadata.msg', type: 'string' }, + ], + }, { name: 'Storage', reference: 'storage_logs', From 97c16123b122f9d3ba6e26ee8bcc32fc7c799c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cemal=20K=C4=B1l=C4=B1=C3=A7?= Date: Wed, 10 Sep 2025 10:34:06 +0200 Subject: [PATCH 11/14] feat(auth): add audit logs configuration page (#37409) * feat(auth): add audit logs configuration page * chore: prettier * fix: config name * Update auth audit logs settings page UI * Update docs URL * feat: use log template for auth audit logs * Nit * Nit * Update field reference for auth audit logs --------- Co-authored-by: Joshen Lim --- .../interfaces/Auth/AuditLogsForm.tsx | 190 ++++++++++++++++++ .../Settings/Logs/Logs.constants.ts | 16 +- .../Settings/Logs/LogsQueryPanel.tsx | 2 +- .../layouts/AuthLayout/AuthLayout.utils.ts | 7 + .../SettingsMenu.utils.tsx | 2 +- apps/studio/components/ui/NoPermission.tsx | 2 +- .../pages/project/[ref]/auth/audit-logs.tsx | 57 ++++++ 7 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 apps/studio/components/interfaces/Auth/AuditLogsForm.tsx create mode 100644 apps/studio/pages/project/[ref]/auth/audit-logs.tsx diff --git a/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx b/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx new file mode 100644 index 0000000000000..85e11b049e065 --- /dev/null +++ b/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx @@ -0,0 +1,190 @@ +import { yupResolver } from '@hookform/resolvers/yup' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useEffect } from 'react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { boolean, object } from 'yup' + +import { useParams } from 'common' +import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' +import { InlineLink } from 'components/ui/InlineLink' +import { NoPermission } from 'components/ui/NoPermission' +import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' +import { useTablesQuery } from 'data/tables/tables-query' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + Card, + CardContent, + CardFooter, + FormControl_Shadcn_, + FormField_Shadcn_, + Form_Shadcn_, + Switch, + WarningIcon, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +const schema = object({ + AUDIT_LOG_DISABLE_POSTGRES: boolean().required(), +}) + +const AUDIT_LOG_ENTRIES_TABLE = 'audit_log_entries' + +export const AuditLogsForm = () => { + const { ref: projectRef } = useParams() + const { data: project } = useSelectedProjectQuery() + const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') + const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + + const { data: tables = [] } = useTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + includeColumns: false, + schema: 'auth', + }) + const auditLogTable = tables.find((x) => x.name === AUDIT_LOG_ENTRIES_TABLE) + + const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + + const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation({ + onError: (error) => { + toast.error(`Failed to update audit logs: ${error?.message}`) + }, + onSuccess: () => { + toast.success('Successfully updated audit logs settings') + }, + }) + + const form = useForm({ + resolver: yupResolver(schema), + defaultValues: { AUDIT_LOG_DISABLE_POSTGRES: false }, + }) + const { AUDIT_LOG_DISABLE_POSTGRES: formValueDisablePostgres } = form.watch() + const currentlyDisabled = authConfig?.AUDIT_LOG_DISABLE_POSTGRES ?? false + const isDisabling = !currentlyDisabled && formValueDisablePostgres + + const onSubmitAuditLogs = (values: any) => { + if (!projectRef) return console.error('Project ref is required') + updateAuthConfig({ projectRef: projectRef, config: values }) + } + + useEffect(() => { + if (authConfig) { + form.reset({ AUDIT_LOG_DISABLE_POSTGRES: authConfig?.AUDIT_LOG_DISABLE_POSTGRES ?? false }) + } + }, [authConfig]) + + if (isError) { + return ( + + + Failed to retrieve auth configuration + {authConfigError.message} + + ) + } + + if (!canReadConfig) { + return + } + + return ( + +
+ Settings + + +
+ + + ( + + Audit logs will no longer be stored in the{' '} + + + {AUDIT_LOG_ENTRIES_TABLE} + + {' '} + table in your project's database, which will reduce database storage + usage. Audit logs will subsequently still be available in the{' '} + + auth logs + + . +

+ } + > + + + +
+ )} + /> + {isDisabling && ( + +

+ Future audit logs will only appear in the project's{' '} + + auth logs + + . You are responsible for backing up, copying, or migrating existing data from + the {AUDIT_LOG_ENTRIES_TABLE} table if needed. +

+
+ )} +
+ + + {form.formState.isDirty && ( + + )} + + +
+
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts index 9a6ae741a997e..bc0eaaa21c075 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -199,6 +199,18 @@ where regexp_contains(event_message,"level.{3}(info|warning||error|fatal)") -- and regexp_contains(event_message,"path.{3}(/token|/recover|/signup|/otp)") limit 100 +`, + for: ['database'], + }, + { + label: 'Auth Audit Logs', + description: 'Audit logs for auth events', + mode: 'custom', + searchString: `select + cast(timestamp as datetime) as timestamp, + event_message, metadata +from auth_audit_logs +limit 10 `, for: ['database'], }, @@ -369,6 +381,7 @@ export enum LogsTableName { FUNCTIONS = 'function_logs', FN_EDGE = 'function_edge_logs', AUTH = 'auth_logs', + AUTH_AUDIT = 'auth_audit_logs', REALTIME = 'realtime_logs', STORAGE = 'storage_logs', POSTGREST = 'postgrest_logs', @@ -400,7 +413,8 @@ export const LOGS_SOURCE_DESCRIPTION = { [LogsTableName.POSTGRES]: 'Database logs obtained directly from Postgres', [LogsTableName.FUNCTIONS]: 'Function logs generated from runtime execution', [LogsTableName.FN_EDGE]: 'Function call logs, containing the request and response', - [LogsTableName.AUTH]: 'Authentication logs from GoTrue', + [LogsTableName.AUTH]: 'Errors, warnings, and performance details from the auth service', + [LogsTableName.AUTH_AUDIT]: 'Audit records of user signups, logins, and account changes', [LogsTableName.REALTIME]: 'Realtime server for Postgres logical replication broadcasting', [LogsTableName.STORAGE]: 'Object storage logs', [LogsTableName.POSTGREST]: 'RESTful API web server logs', diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx index ff2e816a34722..8e1151139b12e 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx @@ -111,7 +111,7 @@ const LogsQueryPanel = ({ {logsTableNames .sort((a, b) => a.localeCompare(b)) diff --git a/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts b/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts index 8bf8dd7da55b6..e40c51f9af4c5 100644 --- a/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts +++ b/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts @@ -108,6 +108,13 @@ export const generateAuthMenu = ( items: [], label: 'BETA', }, + { + name: 'Audit Logs', + key: 'audit-logs', + url: `/project/${ref}/auth/audit-logs`, + items: [], + label: 'BETA', + }, ...(authenticationAdvanced ? [ { diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx index b4a6b32e9fbcc..3ec68b9cc24e2 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx @@ -109,7 +109,7 @@ export const generateSettingsMenu = ( url: isProjectBuilding ? buildingUrl : `/project/${ref}/integrations/vault/overview`, items: [], rightIcon: , - label: 'Alpha', + label: 'ALPHA', }, ], }, diff --git a/apps/studio/components/ui/NoPermission.tsx b/apps/studio/components/ui/NoPermission.tsx index af4fe364fb979..01bc25ce8b04a 100644 --- a/apps/studio/components/ui/NoPermission.tsx +++ b/apps/studio/components/ui/NoPermission.tsx @@ -5,7 +5,7 @@ interface NoPermissionProps { isFullPage?: boolean } -const NoPermission = ({ resourceText, isFullPage = false }: NoPermissionProps) => { +export const NoPermission = ({ resourceText, isFullPage = false }: NoPermissionProps) => { const NoPermissionMessage = () => (
{ + const { ref: projectRef } = useParams() + const isPermissionsLoaded = usePermissionsLoaded() + const { isLoading: isLoadingConfig } = useAuthConfigQuery({ projectRef }) + const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') + + if (isPermissionsLoaded && !canReadAuthSettings) { + return + } + + return ( + + {!isPermissionsLoaded || isLoadingConfig ? ( +
+ +
+ ) : ( + + )} +
+ ) +} + +const secondaryActions = [ + , +] + +AuditLogsPage.getLayout = (page) => ( + + + + {page} + + + +) + +export default AuditLogsPage From a12243d145687a73ffca5c48236e0eb1464ac893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cemal=20K=C4=B1l=C4=B1=C3=A7?= Date: Wed, 10 Sep 2025 11:23:59 +0200 Subject: [PATCH 12/14] fix: update auth audit logs link (#38585) --- apps/studio/components/interfaces/Auth/AuditLogsForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx b/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx index 85e11b049e065..eb9ac33b0963f 100644 --- a/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuditLogsForm.tsx @@ -155,7 +155,7 @@ export const AuditLogsForm = () => { auth logs From 48d76a1c6b753a9cdd388d46adffd22484385d95 Mon Sep 17 00:00:00 2001 From: Riccardo Busetti Date: Wed, 10 Sep 2025 11:26:37 +0200 Subject: [PATCH 13/14] feat(replication): Add UI for updating pipeline versions (#38473) --- .../Database/Replication/DestinationRow.tsx | 31 +- .../Database/Replication/Destinations.tsx | 29 +- .../Replication/ReplicationPipelineStatus.tsx | 404 ++++++++++-------- .../Database/Replication/RowMenu.tsx | 22 +- .../Replication/UpdateVersionModal.tsx | 125 ++++++ apps/studio/data/replication/keys.ts | 2 + .../replication/pipeline-version-query.ts | 50 +++ .../update-pipeline-version-mutation.ts | 74 ++++ 8 files changed, 545 insertions(+), 192 deletions(-) create mode 100644 apps/studio/components/interfaces/Database/Replication/UpdateVersionModal.tsx create mode 100644 apps/studio/data/replication/pipeline-version-query.ts create mode 100644 apps/studio/data/replication/update-pipeline-version-mutation.ts diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx index 28da065f287bb..90a76a0c5f279 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx @@ -8,6 +8,7 @@ import AlertError from 'components/ui/AlertError' import { useDeleteDestinationPipelineMutation } from 'data/replication/delete-destination-pipeline-mutation' import { useReplicationPipelineReplicationStatusQuery } from 'data/replication/pipeline-replication-status-query' import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query' +import { useReplicationPipelineVersionQuery } from 'data/replication/pipeline-version-query' import { Pipeline } from 'data/replication/pipelines-query' import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation' import { AlertCircle } from 'lucide-react' @@ -24,13 +25,14 @@ import { getStatusName, PIPELINE_ERROR_MESSAGES } from './Pipeline.utils' import { PipelineStatus, PipelineStatusName } from './PipelineStatus' import { STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' import { RowMenu } from './RowMenu' +import { UpdateVersionModal } from './UpdateVersionModal' interface DestinationRowProps { - sourceId: number | undefined + sourceId?: number destinationId: number destinationName: string type: string - pipeline: Pipeline | undefined + pipeline?: Pipeline error: ResponseError | null isLoading: boolean isError: boolean @@ -52,6 +54,7 @@ export const DestinationRow = ({ const [showDeleteDestinationForm, setShowDeleteDestinationForm] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [showEditDestinationPanel, setShowEditDestinationPanel] = useState(false) + const [showUpdateVersionModal, setShowUpdateVersionModal] = useState(false) const { data: pipelineStatusData, @@ -66,7 +69,7 @@ export const DestinationRow = ({ }, { refetchInterval: STATUS_REFRESH_FREQUENCY_MS } ) - const { getRequestStatus, updatePipelineStatus } = usePipelineRequestStatus() + const { getRequestStatus, updatePipelineStatus, setRequestStatus } = usePipelineRequestStatus() const requestStatus = pipeline?.id ? getRequestStatus(pipeline.id) : PipelineStatusRequestStatus.None @@ -86,6 +89,13 @@ export const DestinationRow = ({ const errorCount = tableStatuses.filter((t) => t.state?.name === 'error').length const hasTableErrors = errorCount > 0 + // Check if a newer pipeline version is available (one-time check cached for session) + const { data: versionData } = useReplicationPipelineVersionQuery({ + projectRef, + pipelineId: pipeline?.id, + }) + const hasUpdate = Boolean(versionData?.new_version) + const onDeleteClick = async () => { if (!projectRef) { return console.error('Project ref is required') @@ -189,11 +199,14 @@ export const DestinationRow = ({ isError={isPipelineStatusError} onDeleteClick={() => setShowDeleteDestinationForm(true)} onEditClick={() => setShowEditDestinationPanel(true)} + hasUpdate={hasUpdate} + onUpdateClick={() => setShowUpdateVersionModal(true)} />
)} + + setShowEditDestinationPanel(false)} @@ -214,6 +228,17 @@ export const DestinationRow = ({ statusName, }} /> + + setShowUpdateVersionModal(false)} + confirmLabel={ + statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED + ? 'Update and restart' + : 'Update version' + } + /> ) } diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx index f8366bee4b463..c1c133f3f0111 100644 --- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx +++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx @@ -1,5 +1,5 @@ import { Plus, Search } from 'lucide-react' -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useParams } from 'common' import Table from 'components/to-be-cleaned/Table' @@ -7,6 +7,9 @@ import AlertError from 'components/ui/AlertError' import { useReplicationDestinationsQuery } from 'data/replication/destinations-query' import { useReplicationPipelinesQuery } from 'data/replication/pipelines-query' import { useReplicationSourcesQuery } from 'data/replication/sources-query' +import { fetchReplicationPipelineVersion } from 'data/replication/pipeline-version-query' +import { replicationKeys } from 'data/replication/keys' +import { useQueryClient } from '@tanstack/react-query' import { Button, cn, Input_Shadcn_ } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' import { DestinationPanel } from './DestinationPanel' @@ -58,6 +61,30 @@ export const Destinations = () => { destination.name.toLowerCase().includes(filterString.toLowerCase()) ) + // Prefetch pipeline version info for all destinations on first load only + const queryClient = useQueryClient() + const prefetchedRef = useRef(false) + useEffect(() => { + if ( + projectRef && + !prefetchedRef.current && + pipelinesData?.pipelines && + pipelinesData.pipelines.length > 0 && + isPipelinesSuccess + ) { + prefetchedRef.current = true + pipelinesData.pipelines.forEach((p) => { + if (!p?.id) return + queryClient.prefetchQuery({ + queryKey: replicationKeys.pipelinesVersion(projectRef, p.id), + queryFn: ({ signal }) => + fetchReplicationPipelineVersion({ projectRef, pipelineId: p.id }, signal), + staleTime: Infinity, + }) + }) + } + }, [projectRef, pipelinesData?.pipelines, isPipelinesSuccess, queryClient]) + return ( <>
diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx index 0bf7a556003a8..31520c5470e85 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus.tsx @@ -1,5 +1,6 @@ import { Activity, + ArrowUpCircle, Ban, ChevronLeft, ExternalLink, @@ -20,6 +21,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useReplicationPipelineByIdQuery } from 'data/replication/pipeline-by-id-query' import { useReplicationPipelineReplicationStatusQuery } from 'data/replication/pipeline-replication-status-query' import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query' +import { useReplicationPipelineVersionQuery } from 'data/replication/pipeline-version-query' import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation' import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation' import { @@ -39,6 +41,7 @@ import { PipelineStatus, PipelineStatusName } from './PipelineStatus' import { STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' import { TableState } from './ReplicationPipelineStatus.types' import { getDisabledStateConfig, getStatusConfig } from './ReplicationPipelineStatus.utils' +import { UpdateVersionModal } from './UpdateVersionModal' /** * Component for displaying replication pipeline status and table replication details. @@ -47,6 +50,7 @@ import { getDisabledStateConfig, getStatusConfig } from './ReplicationPipelineSt export const ReplicationPipelineStatus = () => { const { ref: projectRef, pipelineId: _pipelineId } = useParams() const [filterString, setFilterString] = useState('') + const [showUpdateVersionModal, setShowUpdateVersionModal] = useState(false) const pipelineId = Number(_pipelineId) const { getRequestStatus, updatePipelineStatus, setRequestStatus } = usePipelineRequestStatus() @@ -89,6 +93,12 @@ export const ReplicationPipelineStatus = () => { } ) + const { data: versionData } = useReplicationPipelineVersionQuery({ + projectRef, + pipelineId: pipeline?.id, + }) + const hasUpdate = Boolean(versionData?.new_version) + const { mutateAsync: startPipeline, isLoading: isStartingPipeline } = useStartPipelineMutation() const { mutateAsync: stopPipeline, isLoading: isStoppingPipeline } = useStopPipelineMutation() @@ -161,213 +171,235 @@ export const ReplicationPipelineStatus = () => { }, [pipelineId, statusName, updatePipelineStatus]) return ( -
-
-
- + <> +
+
-

{destinationName || 'Pipeline'}

- + +
+

{destinationName || 'Pipeline'}

+ +
-
-
- } - className="pl-7 h-[26px] text-xs" - placeholder="Search for tables" - value={filterString} - disabled={isPipelineError} - onChange={(e) => setFilterString(e.target.value)} - actions={ - filterString.length > 0 - ? [ - setFilterString('')} - />, - ] - : undefined - } - /> - +
+ {hasUpdate && ( + + )} + } + className="pl-7 h-[26px] text-xs" + placeholder="Search for tables" + value={filterString} + disabled={isPipelineError} + onChange={(e) => setFilterString(e.target.value)} + actions={ + filterString.length > 0 + ? [ + setFilterString('')} + />, + ] + : undefined + } + /> + + - + +
-
- {(isPipelineLoading || isStatusLoading) && } + {(isPipelineLoading || isStatusLoading) && } - {isPipelineError && ( - - )} + {isPipelineError && ( + + )} - {isStatusError && ( - - )} + {isStatusError && ( + + )} - {hasTableData && ( -
- {showDisabledState && ( -
-
-
-
{config.icon}
-
-
-

{config.title}

-

{config.message}

+ {hasTableData && ( +
+ {showDisabledState && ( +
+
+
+
{config.icon}
+
+
+

{config.title}

+

{config.message}

+
-
- )} - -
- {/* [Joshen] Should update to use new Table components next time */} - Table, - Status, - Details, - ]} - body={ - <> - {filteredTableStatuses.length === 0 && hasTableData && ( - - -
-

No results found

-

- Your search for "{filterString}" did not return any results -

-
-
-
- )} - {filteredTableStatuses.map((table: TableState, index: number) => { - const statusConfig = getStatusConfig(table.state) - return ( - - -
-

{table.table_name}

+ )} - } - tooltip={{ - content: { side: 'bottom', text: 'Open in Table Editor' }, - }} - > - - +
+ {/* [Joshen] Should update to use new Table components next time */} +
Table, + Status, + Details, + ]} + body={ + <> + {filteredTableStatuses.length === 0 && hasTableData && ( + + +
+

No results found

+

+ Your search for "{filterString}" did not return any results +

- - {showDisabledState ? ( - Not Available - ) : ( - statusConfig.badge - )} - - - {showDisabledState ? ( -

- Status unavailable while pipeline is {config.badge.toLowerCase()} -

- ) : ( -
- - )} + )} - {!isStatusLoading && tableStatuses.length === 0 && ( -
-
-
- -
-
-

- {showDisabledState ? 'Pipeline Not Running' : 'No table status information'} -

-

- {showDisabledState - ? `The replication pipeline is currently ${statusName || 'not active'}. Table status + {!isStatusLoading && tableStatuses.length === 0 && ( +

+
+
+ +
+
+

+ {showDisabledState ? 'Pipeline not running' : 'No table status information'} +

+

+ {showDisabledState + ? `The replication pipeline is currently ${statusName || 'not active'}. Table status information is not available while the pipeline is in this state.` - : `This pipeline doesn't have any table replication status data available yet. The status will appear here once replication begins.`} + : `This pipeline doesn't have any table replication status data available yet. The status will appear here once replication begins.`} +

+
+

+ Data refreshes automatically every 2 seconds

-

- Data refreshes automatically every 2 seconds -

-
- )} -
+ )} +
+ setShowUpdateVersionModal(false)} + confirmLabel={ + statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED + ? 'Update and restart' + : 'Update version' + } + /> + ) } diff --git a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx index 203aa40bae676..81f82343cc941 100644 --- a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx +++ b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx @@ -1,4 +1,4 @@ -import { Edit, MoreVertical, Pause, Play, RotateCcw, Trash } from 'lucide-react' +import { Edit, MoreVertical, Pause, Play, RotateCcw, Trash, ArrowUpCircle } from 'lucide-react' import { toast } from 'sonner' import { useParams } from 'common' @@ -38,6 +38,8 @@ interface RowMenuProps { isError: boolean onEditClick: () => void onDeleteClick: () => void + hasUpdate?: boolean + onUpdateClick?: () => void } export const RowMenu = ({ @@ -48,6 +50,8 @@ export const RowMenu = ({ isError, onEditClick, onDeleteClick, + hasUpdate = false, + onUpdateClick, }: RowMenuProps) => { const { ref: projectRef } = useParams() const statusName = getStatusName(pipelineStatus) @@ -138,10 +142,24 @@ export const RowMenu = ({ - - - - {isLoading && ( -
- - - -
- )} + {isLoading || isLoadingPermissions ? ( +
+ + + +
+ ) : !canReadAuditLogs ? ( + + ) : null} - {isError ? ( - error.message.endsWith('upgrade to Team or Enterprise Plan to access audit logs.') ? ( - - -
-
- - Organization Audit Logs are not available on Free or Pro plans - - -

- Upgrade to Team or Enterprise to view up to 28 days of Audit Logs for your - organization. -

-
-
+ {isError ? ( + error.message.endsWith(logsUpgradeError) ? ( + + +
+
+ + Organization Audit Logs are not available on Free or Pro plans + + +

+ Upgrade to Team or Enterprise to view up to 28 days of Audit Logs for your + organization. +

+
+
-
- +
+ +
-
-
- ) : error.message.includes('range exceeded') ? ( - - - Date range too large - - The selected date range exceeds the maximum allowed period. Please select a - smaller time range. - - - ) : ( - - ) - ) : null} - - {isSuccess && ( - <> - {logs.length === 0 ? ( -
-

- Your organization does not have any audit logs available yet -

-
- ) : logs.length > 0 && sortedLogs.length === 0 ? ( -
-

No audit logs found based on the filters applied

-
+ + ) : error.message.includes('range exceeded') ? ( + + + Date range too large + + The selected date range exceeds the maximum allowed period. Please select a + smaller time range. + + ) : ( -
- User - , - - Action - , - - Target - , - -
-

Date

+ + ) + ) : null} - - ) : ( - - ) - } - onClick={() => setDateSortDesc(!dateSortDesc)} - tooltip={{ - content: { - side: 'bottom', - text: dateSortDesc ? 'Sort latest first' : 'Sort earliest first', - }, - }} - /> -
-
, - , - ]} - body={ - sortedLogs?.map((log) => { - const user = (members ?? []).find( - (member) => member.gotrue_id === log.actor.id - ) - const role = roles.find((role) => user?.role_ids?.[0] === role.id) - const project = projects?.find( - (project) => project.ref === log.target.metadata.project_ref - ) - const organization = organizations?.find( - (org) => org.slug === log.target.metadata.org_slug - ) + {isSuccess && ( + <> + {logs.length === 0 ? ( +
+

+ Your organization does not have any audit logs available yet +

+
+ ) : logs.length > 0 && sortedLogs.length === 0 ? ( +
+

+ No audit logs found based on the filters applied +

+
+ ) : ( +
+ User + , + + Action + , + + Target + , + +
+

Date

- const hasStatusCode = log.action.metadata[0]?.status !== undefined - const userIcon = - user === undefined ? ( -
-

?

-
- ) : user?.invited_id || user?.username === user?.primary_email ? ( -
- -
- ) : ( - + ) : ( + + ) + } + onClick={() => setDateSortDesc(!dateSortDesc)} + tooltip={{ + content: { + side: 'bottom', + text: dateSortDesc ? 'Sort latest first' : 'Sort earliest first', + }, + }} /> +
+
, + , + ]} + body={ + sortedLogs?.map((log) => { + const user = (members ?? []).find( + (member) => member.gotrue_id === log.actor.id ) + const role = roles.find((role) => user?.role_ids?.[0] === role.id) + const project = projects?.find( + (project) => project.ref === log.target.metadata.project_ref + ) + const organization = organizations?.find( + (org) => org.slug === log.target.metadata.org_slug + ) + + const hasStatusCode = log.action.metadata[0]?.status !== undefined + const userIcon = + user === undefined ? ( +
+

?

+
+ ) : user?.invited_id || user?.username === user?.primary_email ? ( +
+ +
+ ) : ( + + ) - return ( - setSelectedLog(log)} - className="cursor-pointer hover:!bg-alternative transition duration-100" - > - -
-
{userIcon}
-
-

{user?.username ?? '-'}

- {role && ( -

- {role?.name} + return ( + setSelectedLog(log)} + className="cursor-pointer hover:!bg-alternative transition duration-100" + > + +

+
{userIcon}
+
+

{user?.username ?? '-'}

+ {role && ( +

+ {role?.name} +

+ )} +
+
+ + +
+ {hasStatusCode && ( +

+ {log.action.metadata[0].status}

)} -
-
- - -
- {hasStatusCode && ( -

- {log.action.metadata[0].status} +

+ {log.action.name}

- )} -

- {log.action.name} +

+
+ +

+ {project?.name + ? 'Project: ' + : organization?.name + ? 'Organization: ' + : null} + {project?.name ?? organization?.name ?? 'Unknown'}

-
-
- -

- {project?.name - ? 'Project: ' - : organization?.name - ? 'Organization: ' - : null} - {project?.name ?? organization?.name ?? 'Unknown'} -

-

- {log.target.metadata.project_ref - ? 'Ref: ' - : log.target.metadata.org_slug - ? 'Slug: ' - : null} - {log.target.metadata.project_ref ?? log.target.metadata.org_slug} -

-
- - {dayjs(log.occurred_at).format('DD MMM YYYY, HH:mm:ss')} - - - - -
- ) - }) ?? [] - } - /> - )} - - )} - - +

+ {log.target.metadata.project_ref + ? 'Ref: ' + : log.target.metadata.org_slug + ? 'Slug: ' + : null} + {log.target.metadata.project_ref ?? log.target.metadata.org_slug} +

+ + + {dayjs(log.occurred_at).format('DD MMM YYYY, HH:mm:ss')} + + + + + + ) + }) ?? [] + } + /> + )} + + )} + + + setSelectedLog(undefined)} /> ) } - -export default AuditLogs diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx index 0431d6c4cd056..073177cae61dc 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx @@ -16,10 +16,7 @@ import { useOrganizationCustomerProfileQuery } from 'data/organizations/organiza import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' import { useOrganizationTaxIdQuery } from 'data/organizations/organization-tax-id-query' import { useOrganizationTaxIdUpdateMutation } from 'data/organizations/organization-tax-id-update-mutation' -import { - useAsyncCheckProjectPermissions, - useCheckPermissions, -} from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Button, Card, CardFooter, Form_Shadcn_ as Form } from 'ui' import { @@ -33,9 +30,9 @@ export const BillingCustomerData = () => { const { slug } = useParams() const { data: selectedOrganization } = useSelectedOrganizationQuery() - const { isSuccess: isPermissionsLoaded, can: canReadBillingCustomerData } = + const { can: canReadBillingCustomerData, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.customer') - const canUpdateBillingCustomerData = useCheckPermissions( + const { can: canUpdateBillingCustomerData } = useAsyncCheckProjectPermissions( PermissionAction.BILLING_WRITE, 'stripe.customer' ) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx index 9584fd770d926..2864351ff57ef 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx @@ -18,10 +18,7 @@ import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection import NoPermission from 'components/ui/NoPermission' import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' -import { - useAsyncCheckProjectPermissions, - useCheckPermissions, -} from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { FormMessage_Shadcn_, Input_Shadcn_ } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -45,9 +42,12 @@ const BillingEmail = () => { const { name, billing_email } = selectedOrganization ?? {} - const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') - const { isSuccess: isPermissionsLoaded, can: canReadBillingEmail } = + const { can: canReadBillingEmail, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') + const { can: canUpdateOrganization } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'organizations' + ) const { data: billingCustomer, isLoading: loadingBillingCustomer } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingEmail }) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx index bbf1025d13ae1..63588dedf5fc8 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx @@ -9,12 +9,12 @@ import { useParams } from 'common' import Table from 'components/to-be-cleaned/Table' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH, PRICING_TIER_PRODUCT_IDS } from 'lib/constants' +import { ChevronRight, ExternalLink } from 'lucide-react' import { pricing } from 'shared-data/pricing' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' import { Alert, Button, Collapsible, SidePanel, cn } from 'ui' -import { ExternalLink, ChevronRight } from 'lucide-react' const SPEND_CAP_OPTIONS: { name: string @@ -43,7 +43,7 @@ const SpendCapSidePanel = () => { const [showUsageCosts, setShowUsageCosts] = useState(false) const [selectedOption, setSelectedOption] = useState<'on' | 'off'>() - const canUpdateSpendCap = useCheckPermissions( + const { can: canUpdateSpendCap } = useAsyncCheckProjectPermissions( PermissionAction.BILLING_WRITE, 'stripe.subscriptions' ) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx index 233e7f970478c..094f62f63aa5c 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx @@ -12,10 +12,12 @@ import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' +import { getStripeElementsAppearanceOptions } from 'components/interfaces/Billing/Payment/Payment.utils' +import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useOrganizationCreditTopUpMutation } from 'data/organizations/organization-credit-top-up-mutation' import { subscriptionKeys } from 'data/subscriptions/keys' -import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { STRIPE_PUBLIC_KEY } from 'lib/constants' import { Alert_Shadcn_, @@ -36,10 +38,8 @@ import { Input_Shadcn_, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import PaymentMethodSelection from './Subscription/PaymentMethodSelection' -import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation' -import { getStripeElementsAppearanceOptions } from 'components/interfaces/Billing/Payment/Payment.utils' import type { PaymentMethodElementRef } from '../../Billing/Payment/PaymentMethods/NewPaymentMethodElement' +import PaymentMethodSelection from './Subscription/PaymentMethodSelection' const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) @@ -63,11 +63,10 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => { createPaymentMethod: PaymentMethodElementRef['createPaymentMethod'] }>(null) - const canTopUpCredits = useCheckPermissions( + const { can: canTopUpCredits, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions( PermissionAction.BILLING_WRITE, 'stripe.subscriptions' ) - const isPermissionsLoaded = usePermissionsLoaded() const { mutateAsync: topUpCredits, diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index d084514da4a1b..002dd26dbe0b2 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -18,7 +18,7 @@ import { useOrgPlansQuery } from 'data/subscriptions/org-plans-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import type { OrgPlan } from 'data/subscriptions/types' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { MANAGED_BY } from 'lib/constants/infrastructure' import { formatCurrency } from 'lib/helpers' @@ -60,7 +60,7 @@ const PlanUpdateSidePanel = () => { const [showDowngradeError, setShowDowngradeError] = useState(false) const [selectedTier, setSelectedTier] = useState<'tier_free' | 'tier_pro' | 'tier_team'>() - const canUpdateSubscription = useCheckPermissions( + const { can: canUpdateSubscription } = useAsyncCheckProjectPermissions( PermissionAction.BILLING_WRITE, 'stripe.subscriptions' ) @@ -88,8 +88,6 @@ const PlanUpdateSidePanel = () => { const { data: plans, isLoading: isLoadingPlans } = useOrgPlansQuery({ orgSlug: slug }) const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery({ slug }) - const billingPartner = subscription?.billing_partner - const { data: subscriptionPreview, error: subscriptionPreviewError, diff --git a/apps/studio/components/interfaces/Organization/Documents/SOC2.tsx b/apps/studio/components/interfaces/Organization/Documents/SOC2.tsx index ecf357accf2f4..f08ed28a53e13 100644 --- a/apps/studio/components/interfaces/Organization/Documents/SOC2.tsx +++ b/apps/studio/components/interfaces/Organization/Documents/SOC2.tsx @@ -12,19 +12,19 @@ import { import NoPermission from 'components/ui/NoPermission' import { getDocument } from 'data/documents/document-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Button } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' export const SOC2 = () => { const { data: organization } = useSelectedOrganizationQuery() const slug = organization?.slug + const { mutate: sendEvent } = useSendEventMutation() - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const currentPlan = organization?.plan @@ -51,7 +51,11 @@ export const SOC2 = () => { - {!canReadSubscriptions ? ( + {isLoadingPermissions ? ( +
+ +
+ ) : !canReadSubscriptions ? ( ) : (
diff --git a/apps/studio/components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx b/apps/studio/components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx index 624cdfbc39a93..2e165448d95f8 100644 --- a/apps/studio/components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx +++ b/apps/studio/components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx @@ -11,18 +11,18 @@ import { import NoPermission from 'components/ui/NoPermission' import { getDocument } from 'data/documents/document-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Button } from 'ui' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' export const SecurityQuestionnaire = () => { const { data: organization } = useSelectedOrganizationQuery() const slug = organization?.slug + const { mutate: sendEvent } = useSendEventMutation() - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const currentPlan = organization?.plan @@ -51,7 +51,11 @@ export const SecurityQuestionnaire = () => {
- {!canReadSubscriptions ? ( + {isLoadingPermissions ? ( +
+ +
+ ) : !canReadSubscriptions ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/DataPrivacyForm.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/DataPrivacyForm.tsx index 54247a6d5178d..b90faee201141 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/DataPrivacyForm.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/DataPrivacyForm.tsx @@ -3,13 +3,16 @@ import { useEffect } from 'react' import { FormActions } from 'components/ui/Forms/FormActions' import { useAIOptInForm } from 'hooks/forms/useAIOptInForm' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Card, CardContent, CardFooter, Form_Shadcn_ } from 'ui' import { AIOptInLevelSelector } from './AIOptInLevelSelector' export const DataPrivacyForm = () => { const { form, onSubmit, isUpdating, currentOptInLevel } = useAIOptInForm() - const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + const { can: canUpdateOrganization } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'organizations' + ) const permissionsHelperText = !canUpdateOrganization ? "You need additional permissions to manage this organization's settings" diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx index de767135a0bc1..5fd8de703b868 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx @@ -4,8 +4,9 @@ import { useState } from 'react' import { toast } from 'sonner' import { LOCAL_STORAGE_KEYS } from 'common' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useOrganizationDeleteMutation } from 'data/organizations/organization-delete-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Button, Form, Input, Modal } from 'ui' @@ -23,7 +24,11 @@ export const DeleteOrganizationButton = () => { '' ) - const canDeleteOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + const { can: canDeleteOrganization } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'organizations' + ) + const { mutate: deleteOrganization, isLoading: isDeleting } = useOrganizationDeleteMutation({ onSuccess: () => { toast.success(`Successfully deleted ${orgName}`) @@ -55,9 +60,22 @@ export const DeleteOrganizationButton = () => { return ( <>
- +
{ +export const GeneralSettings = () => { const organizationDeletionEnabled = useIsFeatureEnabled('organizations:delete') - const canDeleteOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') - return ( @@ -31,9 +27,7 @@ const GeneralSettings = () => { - {organizationDeletionEnabled && canDeleteOrganization && } + {organizationDeletionEnabled && } ) } - -export default GeneralSettings diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx index 04e6537c15ca1..d484b76993bea 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx @@ -1,11 +1,11 @@ import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' import PartnerManagedResource from 'components/ui/PartnerManagedResource' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { MANAGED_BY } from 'lib/constants/infrastructure' import { Admonition } from 'ui-patterns' import { DeleteOrganizationButton } from './DeleteOrganizationButton' -import { MANAGED_BY } from 'lib/constants/infrastructure' -const OrganizationDeletePanel = () => { +export const OrganizationDeletePanel = () => { const { data: selectedOrganization } = useSelectedOrganizationQuery() return ( @@ -33,5 +33,3 @@ const OrganizationDeletePanel = () => { ) } - -export default OrganizationDeletePanel diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDetailsForm.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDetailsForm.tsx index 72858b0761f94..797f791e92ec1 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDetailsForm.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDetailsForm.tsx @@ -11,7 +11,7 @@ import CopyButton from 'components/ui/CopyButton' import { FormActions } from 'components/ui/Forms/FormActions' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import type { ResponseError } from 'types' import { @@ -34,7 +34,11 @@ export const OrganizationDetailsForm = () => { const { slug } = useParams() const queryClient = useQueryClient() const { data: selectedOrganization } = useSelectedOrganizationQuery() - const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + + const { can: canUpdateOrganization } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'organizations' + ) const { mutate: updateOrganization, isLoading: isUpdatingDetails } = useOrganizationUpdateMutation() diff --git a/apps/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx b/apps/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx index 88764d28498d4..453b0ed4f3102 100644 --- a/apps/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx +++ b/apps/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx @@ -19,7 +19,7 @@ import { useGitHubAuthorizationQuery } from 'data/integrations/github-authorizat import { useGitHubConnectionDeleteMutation } from 'data/integrations/github-connection-delete-mutation' import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query' import type { IntegrationProjectConnection } from 'data/integrations/integrations.types' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { BASE_PATH } from 'lib/constants' @@ -29,6 +29,7 @@ import { } from 'lib/github' import { useRouter } from 'next/router' import { useSidePanelsStateSnapshot } from 'state/side-panels' +import { GenericSkeletonLoader } from 'ui-patterns' import { IntegrationConnectionItem } from '../../Integrations/VercelGithub/IntegrationConnection' import SidePanelVercelProjectLinker from './SidePanelVercelProjectLinker' @@ -48,15 +49,13 @@ const IntegrationSettings = () => { const showVercelIntegration = useIsFeatureEnabled('integrations:vercel') - const canReadGithubConnection = useCheckPermissions( - PermissionAction.READ, - 'integrations.github_connections' - ) - const canCreateGitHubConnection = useCheckPermissions( + const { can: canReadGithubConnection, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.READ, 'integrations.github_connections') + const { can: canCreateGitHubConnection } = useAsyncCheckProjectPermissions( PermissionAction.CREATE, 'integrations.github_connections' ) - const canUpdateGitHubConnection = useCheckPermissions( + const { can: canUpdateGitHubConnection } = useAsyncCheckProjectPermissions( PermissionAction.UPDATE, 'integrations.github_connections' ) @@ -121,7 +120,9 @@ The GitHub app will watch for changes in your repository such as file changes, b - {!canReadGithubConnection ? ( + {isLoadingPermissions ? ( + + ) : !canReadGithubConnection ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx index ea6ce8776b35b..08a9259f0636a 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx @@ -4,7 +4,7 @@ import { Edit, MoreVertical, Trash } from 'lucide-react' import Table from 'components/to-be-cleaned/Table' import CopyButton from 'components/ui/CopyButton' import type { OAuthApp } from 'data/oauth/oauth-apps-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, DropdownMenu, @@ -25,8 +25,14 @@ export interface OAuthAppRowProps { } export const OAuthAppRow = ({ app, onSelectEdit, onSelectDelete }: OAuthAppRowProps) => { - const canUpdateOAuthApps = useCheckPermissions(PermissionAction.UPDATE, 'approved_oauth_apps') - const canDeleteOAuthApps = useCheckPermissions(PermissionAction.DELETE, 'approved_oauth_apps') + const { can: canUpdateOAuthApps } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'approved_oauth_apps' + ) + const { can: canDeleteOAuthApps } = useAsyncCheckProjectPermissions( + PermissionAction.DELETE, + 'approved_oauth_apps' + ) return ( diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx index 79b4464e083ba..ddeff5064ec9c 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx @@ -3,7 +3,7 @@ import { Check, X } from 'lucide-react' import { useState } from 'react' import { useParams } from 'common' -import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' @@ -13,7 +13,7 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { AuthorizedApp, useAuthorizedAppsQuery } from 'data/oauth/authorized-apps-query' import { OAuthAppCreateResponse } from 'data/oauth/oauth-app-create-mutation' import { OAuthApp, useOAuthAppsQuery } from 'data/oauth/oauth-apps-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, cn } from 'ui' import { AuthorizedAppRow } from './AuthorizedAppRow' import { DeleteAppModal } from './DeleteAppModal' @@ -26,7 +26,7 @@ import { RevokeAppModal } from './RevokeAppModal' // to prevent any confusion (case study: GitHub). Authorized apps could be in the "integrations" tab, but let's // check in again after we wrap up Vercel integration -const OAuthApps = () => { +export const OAuthApps = () => { const { slug } = useParams() const [showPublishModal, setShowPublishModal] = useState(false) const [createdApp, setCreatedApp] = useState() @@ -34,8 +34,12 @@ const OAuthApps = () => { const [selectedAppToDelete, setSelectedAppToDelete] = useState() const [selectedAppToRevoke, setSelectedAppToRevoke] = useState() - const canReadOAuthApps = useCheckPermissions(PermissionAction.READ, 'approved_oauth_apps') - const canCreateOAuthApps = useCheckPermissions(PermissionAction.CREATE, 'approved_oauth_apps') + const { can: canReadOAuthApps, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.READ, 'approved_oauth_apps') + const { can: canCreateOAuthApps } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'approved_oauth_apps' + ) const { data: publishedApps, @@ -60,175 +64,121 @@ const OAuthApps = () => { return Number(new Date(a.authorized_at)) - Number(new Date(b.authorized_at)) }) - if (!canReadOAuthApps) { - return ( - - - - ) - } - return ( <> - -
-
-
-

Published Apps

-

- Build integrations that extend Supabase's functionality -

+ + +
+
+
+

Published Apps

+

+ Build integrations that extend Supabase's functionality +

+
+ setShowPublishModal(true)} + tooltip={{ + content: { + side: 'bottom', + text: !canCreateOAuthApps + ? 'You need additional permissions to create apps' + : undefined, + }, + }} + > + Add application +
- setShowPublishModal(true)} - tooltip={{ - content: { - side: 'bottom', - text: !canCreateOAuthApps - ? 'You need additional permissions to create apps' - : undefined, - }, - }} - > - Add application - -
- {isLoadingPublishedApps && ( -
- - - -
- )} + {isLoadingPublishedApps || isLoadingPermissions ? ( +
+ + + +
+ ) : !canReadOAuthApps ? ( + + ) : null} - {isErrorPublishedApps && ( - - )} + {isErrorPublishedApps && ( + + )} - {createdApp !== undefined && ( -
-
-
-
-
-
- -

You've created your new OAuth application.

-
-

- Ensure that you store the client secret securely - you will not be able to see - it again. -

+ {createdApp !== undefined && ( +
+
+
-
-
-

Client ID

-

{createdApp.client_id}

- +
+
+
+ +

You've created your new OAuth application.

+
+

+ Ensure that you store the client secret securely - you will not be able to see + it again. +

+
+
+

Client ID

+

{createdApp.client_id}

+ +
-
-

Client Secret

-

{createdApp.client_secret}

- +
+

Client Secret

+

{createdApp.client_secret}

+ +
-
- )} - - {isSuccessPublishedApps && ( - <> - {(publishedApps?.length ?? 0) === 0 ? ( -
-

You do not have any published applications yet

-
- ) : ( -
, - Name, - Client ID, - Created at, - , - ]} - body={ - sortedPublishedApps?.map((app) => ( - { - setShowPublishModal(true) - setSelectedAppToUpdate(app) - }} - onSelectDelete={() => setSelectedAppToDelete(app)} - /> - )) ?? [] - } - /> - )} - - )} - - -
-

Authorized Apps

-

- Applications that have access to your organization's settings and projects -

- -
- {isLoadingAuthorizedApps && ( -
- - - -
)} - {isErrorAuthorizedApps && } - - {isSuccessAuthorizedApps && ( + {isSuccessPublishedApps && ( <> - {(authorizedApps.length ?? 0) === 0 ? ( -
-

You do not have any authorized applications yet

+ {(publishedApps?.length ?? 0) === 0 ? ( +
+

You do not have any published applications yet

) : (
, Name, - Created by, - App ID, - Authorized at, + Client ID, + Created at, , ]} body={ - sortedAuthorizedApps?.map((app) => ( - ( + setSelectedAppToRevoke(app)} + onSelectEdit={() => { + setShowPublishModal(true) + setSelectedAppToUpdate(app) + }} + onSelectDelete={() => setSelectedAppToDelete(app)} /> )) ?? [] } @@ -237,8 +187,62 @@ const OAuthApps = () => { )} - - + +
+

Authorized Apps

+

+ Applications that have access to your organization's settings and projects +

+ +
+ {isLoadingAuthorizedApps || isLoadingPermissions ? ( +
+ + + +
+ ) : !canReadOAuthApps ? ( + + ) : null} + + {isErrorAuthorizedApps && } + + {isSuccessAuthorizedApps && ( + <> + {(authorizedApps.length ?? 0) === 0 ? ( +
+

+ You do not have any authorized applications yet +

+
+ ) : ( +
, + Name, + Created by, + App ID, + Authorized at, + , + ]} + body={ + sortedAuthorizedApps?.map((app) => ( + setSelectedAppToRevoke(app)} + /> + )) ?? [] + } + /> + )} + + )} + + + + { ) } - -export default OAuthApps diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthSecrets/OAuthSecrets.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthSecrets/OAuthSecrets.tsx index 9026e2d63d174..84e003a9e8358 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthSecrets/OAuthSecrets.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthSecrets/OAuthSecrets.tsx @@ -7,7 +7,7 @@ import { InlineLink } from 'components/ui/InlineLink' import { useClientSecretCreateMutation } from 'data/oauth-secrets/client-secret-create-mutation' import { CreatedSecret, useClientSecretsQuery } from 'data/oauth-secrets/client-secrets-query' import { OAuthApp } from 'data/oauth/oauth-apps-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Alert_Shadcn_, AlertTitle_Shadcn_, InfoIcon } from 'ui' import { SecretRow } from './SecretRow' @@ -18,7 +18,10 @@ interface Props { export const OAuthSecrets = ({ selectedApp }: Props) => { const { slug } = useParams() const [createdSecret, setCreatedSecret] = useState() - const canManageSecrets = useCheckPermissions(PermissionAction.UPDATE, 'oauth_apps') + const { can: canManageSecrets } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'oauth_apps' + ) const { id: appId } = selectedApp ?? {} diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthSecrets/SecretRow.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthSecrets/SecretRow.tsx index 20717de9729c3..92e3e3a55aa73 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthSecrets/SecretRow.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthSecrets/SecretRow.tsx @@ -10,7 +10,7 @@ import CopyButton from 'components/ui/CopyButton' import { useClientSecretDeleteMutation } from 'data/oauth-secrets/client-secret-delete-mutation' import { Secret, useClientSecretsQuery } from 'data/oauth-secrets/client-secrets-query' import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { cn } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' @@ -22,7 +22,10 @@ export interface SecretRowProps { export const SecretRow = ({ secret, appId }: SecretRowProps) => { const { slug } = useParams() const [showDeleteModal, setShowDeleteModal] = useState(false) - const canManageSecrets = useCheckPermissions(PermissionAction.UPDATE, 'oauth_apps') + const { can: canManageSecrets } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'oauth_apps' + ) const { data } = useClientSecretsQuery({ slug, appId }) const secrets = data?.client_secrets ?? [] diff --git a/apps/studio/components/interfaces/Organization/SecuritySettings/SecuritySettings.tsx b/apps/studio/components/interfaces/Organization/SecuritySettings/SecuritySettings.tsx index e4934a603f201..1dd91caccb024 100644 --- a/apps/studio/components/interfaces/Organization/SecuritySettings/SecuritySettings.tsx +++ b/apps/studio/components/interfaces/Organization/SecuritySettings/SecuritySettings.tsx @@ -7,7 +7,7 @@ import { toast } from 'sonner' import { z } from 'zod' import { useParams } from 'common' -import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' import NoPermission from 'components/ui/NoPermission' @@ -16,7 +16,7 @@ import { useOrganizationMembersQuery } from 'data/organizations/organization-mem import { useOrganizationMfaToggleMutation } from 'data/organizations/organization-mfa-mutation' import { useOrganizationMfaQuery } from 'data/organizations/organization-mfa-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useProfile } from 'lib/profile' import { @@ -42,13 +42,18 @@ const schema = z.object({ enforceMfa: z.boolean(), }) -const SecuritySettings = () => { +export const SecuritySettings = () => { const { slug } = useParams() const { profile } = useProfile() const { data: selectedOrganization } = useSelectedOrganizationQuery() const { data: members } = useOrganizationMembersQuery({ slug }) - const canReadMfaConfig = useCheckPermissions(PermissionAction.READ, 'organizations') - const canUpdateMfaConfig = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + + const { can: canReadMfaConfig, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.READ, 'organizations') + const { can: canUpdateMfaConfig } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'organizations' + ) const { mutate: sendEvent } = useSendEventMutation() const isPaidPlan = selectedOrganization?.plan.id !== 'free' @@ -59,7 +64,7 @@ const SecuritySettings = () => { isLoading: isLoadingMfa, isError: isErrorMfa, isSuccess: isSuccessMfa, - } = useOrganizationMfaQuery({ slug }, { enabled: canReadMfaConfig }) + } = useOrganizationMfaQuery({ slug }, { enabled: isPaidPlan && canReadMfaConfig }) const { mutate: toggleMfa, isLoading: isUpdatingMfa } = useOrganizationMfaToggleMutation({ onError: (error) => { @@ -101,134 +106,130 @@ const SecuritySettings = () => { toggleMfa({ slug, setEnforced: values.enforceMfa }) } - if (!canReadMfaConfig) { - return ( - - - - ) - } - return ( - - {!isPaidPlan && ( - - -
-
- - Organization MFA enforcement is not available on Free plan - - -

Upgrade to Pro or above to enforce MFA requirements for your organization.

-
-
- -
- -
-
-
- )} - - {(isErrorMfa || mfaError) && isPaidPlan && ( - - )} - - {isLoadingMfa && ( - - - - - - )} - - {isSuccessMfa && isPaidPlan && ( - -
- - - ( - - - - -
- -
-
- {(!canUpdateMfaConfig || !hasMFAEnabled) && ( - - {!canUpdateMfaConfig ? ( - "You don't have permission to update MFA settings" - ) : ( - <> - Enable MFA on - your own account first - - )} - - )} -
-
-
- )} - /> -
- - {form.formState.isDirty && ( - - )} - - -
- -
- )} -
+ + + + ) : ( + <> + {isLoadingMfa || isLoadingPermissions ? ( + + + + + + ) : !canReadMfaConfig ? ( + + ) : null} + + {(isErrorMfa || mfaError) && isPaidPlan && ( + + )} + + {isSuccessMfa && isPaidPlan && ( + +
+ + + ( + + + + +
+ +
+
+ {(!canUpdateMfaConfig || !hasMFAEnabled) && ( + + {!canUpdateMfaConfig ? ( + "You don't have permission to update MFA settings" + ) : ( + <> + Enable MFA{' '} + on your own account first + + )} + + )} +
+
+
+ )} + /> +
+ + {form.formState.isDirty && ( + + )} + + +
+ +
+ )} + + )} + + ) } - -export default SecuritySettings diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx index 9b3108a1f3076..24b1a8a9fed71 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx @@ -1,5 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { MoreVertical, Trash } from 'lucide-react' +import { MoreVertical, Redo2, Trash } from 'lucide-react' import { useState } from 'react' import { toast } from 'sonner' @@ -16,7 +16,7 @@ import { } from 'data/organizations/organization-members-query' import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useProjectsQuery } from 'data/projects/projects-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useProfile } from 'lib/profile' @@ -68,14 +68,20 @@ export const MemberActions = ({ member }: MemberActionsProps) => { const roleId = member.role_ids?.[0] ?? -1 const canRemoveMember = member.role_ids.every((id) => rolesRemovable.includes(id)) - const canResendInvite = - useCheckPermissions(PermissionAction.CREATE, 'user_invites', { - resource: { role_id: roleId }, - }) && hasOrgRole - const canRevokeInvite = - useCheckPermissions(PermissionAction.DELETE, 'user_invites', { - resource: { role_id: roleId }, - }) && hasOrgRole + + const { can: canCreateUserInvites } = useAsyncCheckProjectPermissions( + PermissionAction.CREATE, + 'user_invites', + { resource: { role_id: roleId } } + ) + const canResendInvite = canCreateUserInvites && hasOrgRole + + const { can: canDeleteUserInvites } = useAsyncCheckProjectPermissions( + PermissionAction.DELETE, + 'user_invites', + { resource: { role_id: roleId } } + ) + const canRevokeInvite = canDeleteUserInvites && hasOrgRole const { mutate: deleteOrganizationMember, isLoading: isDeletingMember } = useOrganizationMemberDeleteMutation({ @@ -193,30 +199,34 @@ export const MemberActions = ({ member }: MemberActionsProps) => { <> handleRevokeInvitation(member)} + disabled={!canResendInvite} + onClick={() => handleResendInvite(member)} tooltip={{ content: { side: 'left', - text: 'Additional permissions required to cancel invitation', + text: 'Additional permissions required to resend invitation', }, }} > -

Cancel invitation

+ +

Resend invitation

+ + handleResendInvite(member)} + disabled={!canRevokeInvite} + onClick={() => handleRevokeInvitation(member)} tooltip={{ content: { side: 'left', - text: 'Additional permissions required to resend invitation', + text: 'Additional permissions required to cancel invitation', }, }} > -

Resend invitation

+ +

Cancel invitation

) : ( diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index e1a51f973873f..d26883fc6706f 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -11,7 +11,7 @@ import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useProjectsQuery } from 'data/projects/projects-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants/metrics' import { @@ -31,7 +31,7 @@ import Egress from './Egress' import SizeAndCounts from './SizeAndCounts' import { TotalUsage } from './TotalUsage' -const Usage = () => { +export const Usage = () => { const { slug, projectRef } = useParams() const [dateRange, setDateRange] = useState() const [selectedProjectRefInputValue, setSelectedProjectRefInputValue] = useState< @@ -43,10 +43,8 @@ const Usage = () => { const selectedProjectRef = selectedProjectRefInputValue === 'all-projects' ? undefined : selectedProjectRefInputValue - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const { data: organization } = useSelectedOrganizationQuery() const { data, isSuccess } = useProjectsQuery() @@ -62,16 +60,6 @@ const Usage = () => { (project) => project.organization_id === organization?.id ) - useEffect(() => { - if (projectRef && isSuccess && orgProjects !== undefined) { - if (orgProjects.find((project) => project.ref === projectRef)) { - setSelectedProjectRefInputValue(projectRef) - } - } - // [Joshen] Since we're already looking at isSuccess - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [projectRef, isSuccess]) - const billingCycleStart = useMemo(() => { return dayjs.unix(subscription?.current_period_start ?? 0).utc() }, [subscription]) @@ -119,15 +107,15 @@ const Usage = () => { ? orgProjects?.find((it) => it.ref === selectedProjectRef) : undefined - if (!canReadSubscriptions) { - return ( - - - - - - ) - } + useEffect(() => { + if (projectRef && isSuccess && orgProjects !== undefined) { + if (orgProjects.find((project) => project.ref === projectRef)) { + setSelectedProjectRefInputValue(projectRef) + } + } + // [Joshen] Since we're already looking at isSuccess + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [projectRef, isSuccess]) return ( <> @@ -139,7 +127,7 @@ const Usage = () => {
- {isLoadingSubscription && ( + {isLoadingSubscription || isLoadingPermissions ? (
@@ -147,7 +135,9 @@ const Usage = () => {
- )} + ) : !canReadSubscriptions ? ( + + ) : null} {isErrorSubscription && ( { ) } - -export default Usage diff --git a/apps/studio/components/interfaces/Organization/index.ts b/apps/studio/components/interfaces/Organization/index.ts index 31287315dc01f..6d2f34a57dae5 100644 --- a/apps/studio/components/interfaces/Organization/index.ts +++ b/apps/studio/components/interfaces/Organization/index.ts @@ -1,9 +1,4 @@ -export { default as AuditLogs } from './AuditLogs/AuditLogs' export { default as Documents } from './Documents/Documents' -export { default as GeneralSettings } from './GeneralSettings/GeneralSettings' -export { default as SecuritySettings } from './SecuritySettings/SecuritySettings' export { default as IntegrationSettings } from './IntegrationSettings/IntegrationSettings' export { default as InvoicesSettings } from './InvoicesSettings/InvoicesSettings' export { default as NewOrgForm } from './NewOrg/NewOrgForm' -export { default as OAuthApps } from './OAuthApps/OAuthApps' -export { default as Usage } from './Usage/Usage' diff --git a/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx index eff4dd29ec761..848edfc8dac42 100644 --- a/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx @@ -12,20 +12,6 @@ function OrganizationSettingsLayout({ children }: PropsWithChildren) { const fullCurrentPath = useCurrentPath() const [currentPath] = fullCurrentPath.split('#') - // Hide these settings in the new layout on the following paths - const isHidden = (path: string) => { - return ( - path === `/org/${slug}/team` || - path === `/org/${slug}/integrations` || - path === `/org/${slug}/usage` || - path === `/org/${slug}/billing` - ) - } - - if (isHidden(currentPath)) { - return children - } - const navMenuItems = [ { label: 'General', diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index 656f395ae4e8d..2727eb420285f 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -2,15 +2,11 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { PropsWithChildren } from 'react' import NoPermission from 'components/ui/NoPermission' -import { - useAsyncCheckProjectPermissions, - usePermissionsLoaded, -} from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { ProjectLayoutWithAuth } from '../ProjectLayout/ProjectLayout' const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { - const isPermissionsLoaded = usePermissionsLoaded() - const { can: canReadTables } = useAsyncCheckProjectPermissions( + const { can: canReadTables, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions( PermissionAction.TENANT_SQL_ADMIN_READ, 'tables' ) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIOptInModal.tsx b/apps/studio/components/ui/AIAssistantPanel/AIOptInModal.tsx index 1b09490417d37..919b847f50b26 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIOptInModal.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIOptInModal.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react' import { AIOptInLevelSelector } from 'components/interfaces/Organization/GeneralSettings/AIOptInLevelSelector' import { useAIOptInForm } from 'hooks/forms/useAIOptInForm' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, cn, @@ -24,7 +24,10 @@ interface AIOptInModalProps { export const AIOptInModal = ({ visible, onCancel }: AIOptInModalProps) => { const { form, onSubmit, isUpdating, currentOptInLevel } = useAIOptInForm(onCancel) - const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + const { can: canUpdateOrganization } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'organizations' + ) const onOpenChange = (open: boolean) => { if (!open) { diff --git a/apps/studio/data/organizations/organization-customer-profile-query.ts b/apps/studio/data/organizations/organization-customer-profile-query.ts index 0780ba2127710..b95c9a7a15efe 100644 --- a/apps/studio/data/organizations/organization-customer-profile-query.ts +++ b/apps/studio/data/organizations/organization-customer-profile-query.ts @@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, handleError } from 'data/fetchers' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import type { ResponseError } from 'types' import { organizationKeys } from './keys' @@ -43,7 +43,7 @@ export const useOrganizationCustomerProfileQuery = { // [Joshen] Thinking it makes sense to add this check at the RQ level - prevent // unnecessary requests, although this behaviour still needs handling on the UI - const canReadCustomerProfile = useCheckPermissions( + const { can: canReadCustomerProfile } = useAsyncCheckProjectPermissions( PermissionAction.BILLING_READ, 'stripe.customer' ) diff --git a/apps/studio/data/organizations/organization-payment-methods-query.ts b/apps/studio/data/organizations/organization-payment-methods-query.ts index dc59d53050609..800fd895f0f25 100644 --- a/apps/studio/data/organizations/organization-payment-methods-query.ts +++ b/apps/studio/data/organizations/organization-payment-methods-query.ts @@ -3,7 +3,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { components } from 'api-types' import { get, handleError } from 'data/fetchers' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import type { ResponseError } from 'types' import { organizationKeys } from './keys' @@ -44,7 +44,7 @@ export const useOrganizationPaymentMethodsQuery = = {} ) => { - const canReadSubscriptions = useCheckPermissions( + const { can: canReadSubscriptions } = useAsyncCheckProjectPermissions( PermissionAction.BILLING_READ, 'stripe.payment_methods' ) diff --git a/apps/studio/data/organizations/organization-tax-id-query.ts b/apps/studio/data/organizations/organization-tax-id-query.ts index e672b0611aae6..06e7ca658964a 100644 --- a/apps/studio/data/organizations/organization-tax-id-query.ts +++ b/apps/studio/data/organizations/organization-tax-id-query.ts @@ -3,7 +3,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { components } from 'api-types' import { get, handleError } from 'data/fetchers' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import type { ResponseError } from 'types' import { organizationKeys } from './keys' @@ -36,7 +36,11 @@ export const useOrganizationTaxIdQuery = ( ...options }: UseQueryOptions = {} ) => { - const canReadSubscriptions = useCheckPermissions(PermissionAction.BILLING_READ, 'stripe.tax_ids') + const { can: canReadSubscriptions } = useAsyncCheckProjectPermissions( + PermissionAction.BILLING_READ, + 'stripe.tax_ids' + ) + return useQuery( organizationKeys.taxId(slug), ({ signal }) => getOrganizationTaxId({ slug }, signal), diff --git a/apps/studio/data/storage/iceberg-wrapper-create-mutation.ts b/apps/studio/data/storage/iceberg-wrapper-create-mutation.ts index 0601d3e87a4cc..ed59e90e08c85 100644 --- a/apps/studio/data/storage/iceberg-wrapper-create-mutation.ts +++ b/apps/studio/data/storage/iceberg-wrapper-create-mutation.ts @@ -9,7 +9,7 @@ import { import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { FDWCreateVariables, useFDWCreateMutation } from 'data/fdw/fdw-create-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useS3AccessKeyCreateMutation } from './s3-access-key-create-mutation' @@ -27,7 +27,10 @@ export const useIcebergWrapperCreateMutation = () => { const wrapperMeta = WRAPPERS.find((wrapper) => wrapper.name === 'iceberg_wrapper') - const canCreateCredentials = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canCreateCredentials } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) const { mutateAsync: createS3AccessKey, isLoading: isCreatingS3AccessKey } = useS3AccessKeyCreateMutation() diff --git a/apps/studio/data/subscriptions/org-plans-query.ts b/apps/studio/data/subscriptions/org-plans-query.ts index 77c98d2c630bd..a79e76e95272c 100644 --- a/apps/studio/data/subscriptions/org-plans-query.ts +++ b/apps/studio/data/subscriptions/org-plans-query.ts @@ -1,8 +1,9 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQuery, UseQueryOptions } from '@tanstack/react-query' + import { get, handleError } from 'data/fetchers' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { subscriptionKeys } from './keys' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { PermissionAction } from '@supabase/shared-types/out/constants' export type OrgPlansVariables = { orgSlug?: string @@ -26,7 +27,7 @@ export const useOrgPlansQuery = ( { orgSlug }: OrgPlansVariables, { enabled = true, ...options }: UseQueryOptions = {} ) => { - const canReadSubscriptions = useCheckPermissions( + const { can: canReadSubscriptions } = useAsyncCheckProjectPermissions( PermissionAction.BILLING_READ, 'stripe.subscriptions' ) diff --git a/apps/studio/data/subscriptions/org-subscription-query.ts b/apps/studio/data/subscriptions/org-subscription-query.ts index 26b6811461279..a91e6f29e7fe0 100644 --- a/apps/studio/data/subscriptions/org-subscription-query.ts +++ b/apps/studio/data/subscriptions/org-subscription-query.ts @@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, handleError } from 'data/fetchers' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import type { ResponseError } from 'types' import { subscriptionKeys } from './keys' @@ -37,7 +37,7 @@ export const useOrgSubscriptionQuery = ( ) => { // [Joshen] Thinking it makes sense to add this check at the RQ level - prevent // unnecessary requests, although this behaviour still needs handling on the UI - const canReadSubscriptions = useCheckPermissions( + const { can: canReadSubscriptions } = useAsyncCheckProjectPermissions( PermissionAction.BILLING_READ, 'stripe.subscriptions' ) diff --git a/apps/studio/hooks/forms/useAIOptInForm.ts b/apps/studio/hooks/forms/useAIOptInForm.ts index bd602497bb716..9a85468fb1777 100644 --- a/apps/studio/hooks/forms/useAIOptInForm.ts +++ b/apps/studio/hooks/forms/useAIOptInForm.ts @@ -8,7 +8,7 @@ import * as z from 'zod' import { LOCAL_STORAGE_KEYS } from 'common' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { getAiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -31,7 +31,10 @@ export type AIOptInFormValues = z.infer export const useAIOptInForm = (onSuccessCallback?: () => void) => { const queryClient = useQueryClient() const { data: selectedOrganization } = useSelectedOrganizationQuery() - const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + const { can: canUpdateOrganization } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'organizations' + ) const [_, setUpdatedOptInSinceMCP] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.AI_ASSISTANT_MCP_OPT_IN, diff --git a/apps/studio/hooks/misc/useCheckPermissions.ts b/apps/studio/hooks/misc/useCheckPermissions.ts index 9ebf33696fb00..15437b5f3c980 100644 --- a/apps/studio/hooks/misc/useCheckPermissions.ts +++ b/apps/studio/hooks/misc/useCheckPermissions.ts @@ -89,40 +89,42 @@ export function useGetProjectPermissions( }) const permissions = permissionsOverride === undefined ? data : permissionsOverride - const organizationsQueryEnabled = organizationSlugOverride === undefined && enabled + const getOrganizationDataFromParamsSlug = organizationSlugOverride === undefined && enabled const { data: organizationData, isLoading: isLoadingOrganization, isSuccess: isSuccessOrganization, } = useSelectedOrganizationQuery({ - enabled: organizationsQueryEnabled, + enabled: getOrganizationDataFromParamsSlug, }) const organization = organizationSlugOverride === undefined ? organizationData : { slug: organizationSlugOverride } const organizationSlug = organization?.slug - const projectsQueryEnabled = projectRefOverride === undefined && enabled + const { ref: urlProjectRef } = useParams() + const getProjectDataFromParamsRef = !!urlProjectRef && projectRefOverride === undefined && enabled const { data: projectData, isLoading: isLoadingProject, isSuccess: isSuccessProject, } = useSelectedProjectQuery({ - enabled: projectsQueryEnabled, + enabled: getProjectDataFromParamsRef, }) const project = projectRefOverride === undefined || projectData?.parent_project_ref ? projectData : { ref: projectRefOverride, parent_project_ref: undefined } + const projectRef = project?.parent_project_ref ? project.parent_project_ref : project?.ref const isLoading = isLoadingPermissions || - (organizationsQueryEnabled && isLoadingOrganization) || - (projectsQueryEnabled && isLoadingProject) + (getOrganizationDataFromParamsSlug && isLoadingOrganization) || + (getProjectDataFromParamsRef && isLoadingProject) const isSuccess = isSuccessPermissions && - (!organizationsQueryEnabled || isSuccessOrganization) && - (!projectsQueryEnabled || isSuccessProject) + (!getOrganizationDataFromParamsSlug || isSuccessOrganization) && + (!getProjectDataFromParamsRef || isSuccessProject) return { permissions, @@ -138,6 +140,7 @@ export function useGetProjectPermissions( * check for loading states to not prematurely show "no perms" UIs. We'll also need a separate async check for org perms too * * Use `import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'` instead + * [Joshen] No longer being used, can be deprecated in follow up PR */ export function useCheckPermissions( action: string, @@ -180,6 +183,7 @@ export function useCheckProjectPermissions( return doPermissionsCheck(allPermissions, action, resource, data, _organizationSlug, _projectRef) } +/** [Joshen] No longer being used, can be deprecated in follow up PR */ export function usePermissionsLoaded() { const isLoggedIn = useIsLoggedIn() const { isFetched: isPermissionsFetched } = usePermissionsQuery({ enabled: isLoggedIn }) @@ -200,6 +204,7 @@ export function usePermissionsLoaded() { return isLoggedIn && isPermissionsFetched && isOrganizationsFetched } +/** [Joshen] To be renamed to be useAsyncCheckPermissions, more generic as it covers both org and project perms */ // Useful when you want to avoid layout changes while waiting for permissions to load export function useAsyncCheckProjectPermissions( action: string, diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 297dc3d709dba..beaa6dacab5d7 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -49,7 +49,7 @@ import { import { useProjectsQuery } from 'data/projects/projects-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCustomContent } from 'hooks/custom-content/useCustomContent' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -260,7 +260,7 @@ const Wizard: NextPageWithLayout = () => { ? availableRegionsData?.recommendations.smartGroup.name : _defaultRegion - const isAdmin = useCheckPermissions(PermissionAction.CREATE, 'projects') + const { can: isAdmin } = useAsyncCheckProjectPermissions(PermissionAction.CREATE, 'projects') const isInvalidSlug = isOrganizationsSuccess && currentOrg === undefined const orgNotFound = isOrganizationsSuccess && (organizations?.length ?? 0) > 0 && isInvalidSlug diff --git a/apps/studio/pages/org/[slug]/apps.tsx b/apps/studio/pages/org/[slug]/apps.tsx index 616f81fca0415..bbbfad68bf625 100644 --- a/apps/studio/pages/org/[slug]/apps.tsx +++ b/apps/studio/pages/org/[slug]/apps.tsx @@ -1,19 +1,11 @@ -import { OAuthApps } from 'components/interfaces/Organization' +import { OAuthApps } from 'components/interfaces/Organization/OAuthApps/OAuthApps' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' -import { Loading } from 'components/ui/Loading' -import { usePermissionsQuery } from 'data/permissions/permissions-query' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import type { NextPageWithLayout } from 'types' const OrgOAuthApps: NextPageWithLayout = () => { - const { data: selectedOrganization } = useSelectedOrganizationQuery() - const { isLoading: isLoadingPermissions } = usePermissionsQuery() - - return ( - <>{selectedOrganization === undefined && isLoadingPermissions ? : } - ) + return } OrgOAuthApps.getLayout = (page) => ( diff --git a/apps/studio/pages/org/[slug]/audit.tsx b/apps/studio/pages/org/[slug]/audit.tsx index 0baf587a80fed..42a813754af8d 100644 --- a/apps/studio/pages/org/[slug]/audit.tsx +++ b/apps/studio/pages/org/[slug]/audit.tsx @@ -1,4 +1,4 @@ -import { AuditLogs } from 'components/interfaces/Organization' +import { AuditLogs } from 'components/interfaces/Organization/AuditLogs/AuditLogs' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' diff --git a/apps/studio/pages/org/[slug]/billing.tsx b/apps/studio/pages/org/[slug]/billing.tsx index 6d692fa5fcf9e..90bbae4879389 100644 --- a/apps/studio/pages/org/[slug]/billing.tsx +++ b/apps/studio/pages/org/[slug]/billing.tsx @@ -4,7 +4,6 @@ import { useParams } from 'common' import { BillingSettings } from 'components/interfaces/Organization/BillingSettings/BillingSettings' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { @@ -37,9 +36,7 @@ const OrgBillingSettings: NextPageWithLayout = () => { OrgBillingSettings.getLayout = (page) => ( - - {page} - + {page} ) export default OrgBillingSettings diff --git a/apps/studio/pages/org/[slug]/general.tsx b/apps/studio/pages/org/[slug]/general.tsx index 73670d676f2c4..1bdcce2af5be1 100644 --- a/apps/studio/pages/org/[slug]/general.tsx +++ b/apps/studio/pages/org/[slug]/general.tsx @@ -1,4 +1,4 @@ -import { GeneralSettings as GeneralSettingsLegacy } from 'components/interfaces/Organization' +import { GeneralSettings } from 'components/interfaces/Organization/GeneralSettings/GeneralSettings' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' @@ -16,7 +16,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => { {selectedOrganization === undefined && isLoadingPermissions ? ( ) : ( - + )} ) diff --git a/apps/studio/pages/org/[slug]/integrations.tsx b/apps/studio/pages/org/[slug]/integrations.tsx index 39964b49f0eb9..69990a65094ff 100644 --- a/apps/studio/pages/org/[slug]/integrations.tsx +++ b/apps/studio/pages/org/[slug]/integrations.tsx @@ -1,8 +1,6 @@ import { IntegrationSettings } from 'components/interfaces/Organization' -import AppLayout from 'components/layouts/AppLayout/AppLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' import type { NextPageWithLayout } from 'types' const OrgIntegrationSettings: NextPageWithLayout = () => { @@ -11,9 +9,7 @@ const OrgIntegrationSettings: NextPageWithLayout = () => { OrgIntegrationSettings.getLayout = (page) => ( - - {page} - + {page} ) diff --git a/apps/studio/pages/org/[slug]/security.tsx b/apps/studio/pages/org/[slug]/security.tsx index 93bf795e639c8..4948e3e1d349d 100644 --- a/apps/studio/pages/org/[slug]/security.tsx +++ b/apps/studio/pages/org/[slug]/security.tsx @@ -1,25 +1,11 @@ -import { SecuritySettings } from 'components/interfaces/Organization' +import { SecuritySettings } from 'components/interfaces/Organization/SecuritySettings/SecuritySettings' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' -import { Loading } from 'components/ui/Loading' -import { usePermissionsQuery } from 'data/permissions/permissions-query' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import type { NextPageWithLayout } from 'types' const OrgGeneralSettings: NextPageWithLayout = () => { - const { isLoading: isLoadingPermissions } = usePermissionsQuery() - const { data: selectedOrganization } = useSelectedOrganizationQuery() - - return ( - <> - {selectedOrganization === undefined && isLoadingPermissions ? ( - - ) : ( - - )} - - ) + return } OrgGeneralSettings.getLayout = (page) => ( diff --git a/apps/studio/pages/org/[slug]/team.tsx b/apps/studio/pages/org/[slug]/team.tsx index aef184ae3a8af..36039aebd48e0 100644 --- a/apps/studio/pages/org/[slug]/team.tsx +++ b/apps/studio/pages/org/[slug]/team.tsx @@ -1,7 +1,6 @@ import { TeamSettings } from 'components/interfaces/Organization/TeamSettings/TeamSettings' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' import { Loading } from 'components/ui/Loading' import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -16,9 +15,7 @@ const OrgTeamSettings: NextPageWithLayout = () => { OrgTeamSettings.getLayout = (page) => ( - - {page} - + {page} ) diff --git a/apps/studio/pages/org/[slug]/usage.tsx b/apps/studio/pages/org/[slug]/usage.tsx index 9936b9e1a7a4e..3f803525bc524 100644 --- a/apps/studio/pages/org/[slug]/usage.tsx +++ b/apps/studio/pages/org/[slug]/usage.tsx @@ -1,24 +1,15 @@ -import { Usage } from 'components/interfaces/Organization' +import { Usage } from 'components/interfaces/Organization/Usage/Usage' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' -import { Loading } from 'components/ui/Loading' -import { usePermissionsQuery } from 'data/permissions/permissions-query' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import type { NextPageWithLayout } from 'types' const OrgUsage: NextPageWithLayout = () => { - const { isLoading: isLoadingPermissions } = usePermissionsQuery() - const { data: selectedOrganization } = useSelectedOrganizationQuery() - - return <>{selectedOrganization === undefined && isLoadingPermissions ? : } + return } OrgUsage.getLayout = (page) => ( - - {page} - + {page} )