diff --git a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts index bd82fbd462199..a1a8500395e0d 100644 --- a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts +++ b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts @@ -28,18 +28,28 @@ function parseMarkdown(markdown: string) { } }) + return { heading, content: withoutFrontmatter.trim() } +} + +/** + * Wraps content in a markdown code block. + * + * Uses `mdast` to ensure proper escaping of backticks within the content. + */ +export function wrapInMarkdownCodeBlock(content: string) { + const mdast = fromMarkdown(content) + const codeBlock: Code = { type: 'code', lang: 'markdown', - value: markdown, + value: content, } const root: Root = { type: 'root', children: [codeBlock], } - const content = toMarkdown(root) - return { heading, content } + return toMarkdown(root) } async function getAiPromptsImpl() { @@ -106,3 +116,28 @@ export async function generateAiPromptsStaticParams() { } }) } + +/** + * Generates a deep link URL for Cursor that preloads the given prompt text. + * + * Cursor deep links have a maximum URL length of 8000 characters. + * If the generated URL exceeds this length, `url` will be undefined + * and an error will be returned. + */ +export function generateCursorPromptDeepLink(promptText: string) { + // Temporarily reject prompts that contain ".env" due to a bug in Cursor + if (promptText.includes('.env')) { + return { error: new Error('Prompt text cannot contain the text .env due to a temporary bug') } + } + + const url = new URL('cursor://anysphere.cursor-deeplink/prompt') + url.searchParams.set('text', promptText) + const urlString = url.toString() + + // Cursor has a max URL length of 8000 characters for deep links + if (urlString.length > 8000) { + return { error: new Error('Prompt text is too long to generate a Cursor deep link.') } + } + + return { url: urlString } +} diff --git a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx index 055a4231d5afe..82f3837575dad 100644 --- a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx +++ b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx @@ -1,9 +1,12 @@ +import { source } from 'common-tags' import { notFound } from 'next/navigation' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { generateAiPromptMetadata, generateAiPromptsStaticParams, + generateCursorPromptDeepLink, getAiPrompt, + wrapInMarkdownCodeBlock, } from './AiPrompts.utils' export const dynamicParams = false @@ -19,17 +22,28 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str } let { heading, content } = prompt - content = ` -## How to use + const { url: cursorUrl } = generateCursorPromptDeepLink(content) -Copy the prompt to a file in your repo. + content = source` + ## How to use -Use the "include file" feature from your AI tool to include the prompt when chatting with your AI assistant. For example, with GitHub Copilot, use \`#\`, in Cursor, use \`@Files\`, and in Zed, use \`/file\`. + Copy the prompt to a file in your repo. -## Prompt + Use the "include file" feature from your AI tool to include the prompt when chatting with your AI assistant. For example, with GitHub Copilot, use \`#\`, in Cursor, use \`@Files\`, and in Zed, use \`/file\`. -${content} -`.trim() + ${ + cursorUrl + ? source` + You can also load the prompt directly into your IDE via the following links: + - [Open in Cursor](${cursorUrl}) + ` + : '' + } + + ## Prompt + + ${wrapInMarkdownCodeBlock(content)} + ` return ( { const { ref } = useParams() + const { docsRowLevelSecurityGuidePath } = useCustomContent(['docs:row_level_security_guide_path']) + return (
@@ -18,7 +21,7 @@ export const CLSPreview = () => { /> diff --git a/apps/studio/components/interfaces/Auth/Overview/OverviewLearnMore.tsx b/apps/studio/components/interfaces/Auth/Overview/OverviewLearnMore.tsx index ab58f60630ab4..8678bc08b49aa 100644 --- a/apps/studio/components/interfaces/Auth/Overview/OverviewLearnMore.tsx +++ b/apps/studio/components/interfaces/Auth/Overview/OverviewLearnMore.tsx @@ -6,17 +6,29 @@ import { Logs } from 'icons' import { BASE_PATH } from 'lib/constants' import { useParams } from 'common' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' +import { useTheme } from 'next-themes' +import { useEffect, useState } from 'react' export const OverviewLearnMore = () => { + const [isMounted, setIsMounted] = useState(false) const { ref } = useParams() const aiSnap = useAiAssistantStateSnapshot() + const { theme, resolvedTheme } = useTheme() + + useEffect(() => { + setIsMounted(true) + }, []) + + const isLight = resolvedTheme === 'light' const LearnMoreCards = [ { label: 'Docs', title: 'Auth docs', description: 'Read more on Supabase auth, managing users and more.', - image: `${BASE_PATH}/img/auth-overview/auth-overview-docs.jpg`, + image: isLight + ? `${BASE_PATH}/img/auth-overview/auth-overview-docs-light.jpg` + : `${BASE_PATH}/img/auth-overview/auth-overview-docs.jpg`, actions: [ { label: 'Docs', @@ -29,7 +41,9 @@ export const OverviewLearnMore = () => { label: 'Assistant', title: 'Explain auth errors', description: 'Our Assistant can help you debug and fix authentication errors.', - image: `${BASE_PATH}/img/auth-overview/auth-overview-assistant.jpg`, + image: isLight + ? `${BASE_PATH}/img/auth-overview/auth-overview-assistant-light.jpg` + : `${BASE_PATH}/img/auth-overview/auth-overview-assistant.jpg`, actions: [ { label: 'Ask Assistant', @@ -70,7 +84,9 @@ export const OverviewLearnMore = () => { label: 'Logs', title: 'Dive into the logs', description: 'Auth logs provide a deeper view into your auth requests.', - image: `${BASE_PATH}/img/auth-overview/auth-overview-logs.jpg`, + image: isLight + ? `${BASE_PATH}/img/auth-overview/auth-overview-logs-light.jpg` + : `${BASE_PATH}/img/auth-overview/auth-overview-logs.jpg`, actions: [ { label: 'Go to logs', @@ -81,6 +97,8 @@ export const OverviewLearnMore = () => { }, ] + if (!isMounted) return null + return ( Learn more diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx index 62a0fb75d629b..773b62437f09b 100644 --- a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx @@ -242,9 +242,9 @@ const NewPaymentMethodElement = forwardRef( }, [availableTaxIds, stripeAddress]) return ( -
-

- Please ensure CVC and postal codes match what is on file for your card. +

+

+ Please ensure CVC and postal codes match what’s on file for your card.

{fullyLoaded && ( -
+
setPurchasingAsBusiness(!purchasingAsBusiness)} /> -
)} diff --git a/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx b/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx index 7e131ff9992c2..230d433b40879 100644 --- a/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -1,4 +1,3 @@ -import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { Clock } from 'lucide-react' import { useRouter } from 'next/router' @@ -10,7 +9,7 @@ import Panel from 'components/ui/Panel' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation' import { DatabaseBackup, useBackupsQuery } from 'data/database/backups-query' -import { setProjectStatus } from 'data/projects/projects-query' +import { useSetProjectStatus } from 'data/projects/project-detail-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' @@ -20,14 +19,13 @@ import { BackupsStorageAlert } from './BackupsStorageAlert' const BackupsList = () => { const router = useRouter() - const queryClient = useQueryClient() const { ref: projectRef } = useParams() + const [selectedBackup, setSelectedBackup] = useState() + const { setProjectStatus } = useSetProjectStatus() const { data: selectedProject } = useSelectedProjectQuery() const isHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY - const [selectedBackup, setSelectedBackup] = useState() - const { data: backups } = useBackupsQuery({ projectRef }) const { mutate: restoreFromBackup, @@ -37,7 +35,7 @@ const BackupsList = () => { onSuccess: () => { if (projectRef) { setTimeout(() => { - setProjectStatus(queryClient, projectRef, PROJECT_STATUS.RESTORING) + setProjectStatus({ ref: projectRef, status: PROJECT_STATUS.RESTORING }) toast.success( `Restoring database back to ${dayjs(selectedBackup?.inserted_at).format( 'DD MMM YYYY HH:mm:ss' diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx index fa2819c877f57..e0dd4198352ee 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx @@ -1,4 +1,3 @@ -import { useQueryClient } from '@tanstack/react-query' import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' @@ -7,7 +6,7 @@ import { useParams } from 'common' import { FormHeader } from 'components/ui/Forms/FormHeader' import { useBackupsQuery } from 'data/database/backups-query' import { usePitrRestoreMutation } from 'data/database/pitr-restore-mutation' -import { setProjectStatus } from 'data/projects/projects-query' +import { useSetProjectStatus } from 'data/projects/project-detail-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { PROJECT_STATUS } from 'lib/constants' import { @@ -28,10 +27,11 @@ import { PITRForm } from './pitr-form' const PITRSelection = () => { const router = useRouter() const { ref } = useParams() - const queryClient = useQueryClient() const { data: backups } = useBackupsQuery({ projectRef: ref }) const { data: databases } = useReadReplicasQuery({ projectRef: ref }) + const { setProjectStatus } = useSetProjectStatus() + const [showConfiguration, setShowConfiguration] = useState(false) const [showConfirmation, setShowConfirmation] = useState(false) const [selectedTimezone, setSelectedTimezone] = useState(getClientTimezone()) @@ -48,10 +48,10 @@ const PITRSelection = () => { isLoading: isRestoring, isSuccess: isSuccessPITR, } = usePitrRestoreMutation({ - onSuccess: (res, variables) => { + onSuccess: (_, variables) => { setTimeout(() => { setShowConfirmation(false) - setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.RESTORING) + setProjectStatus({ ref: variables.ref, status: PROJECT_STATUS.RESTORING }) router.push(`/project/${variables.ref}`) }, 3000) }, diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index cee0f6f73c742..f02ce81f0ada1 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -1,6 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useQueryClient } from '@tanstack/react-query' import { AnimatePresence, motion } from 'framer-motion' import { ChevronRight } from 'lucide-react' import { useEffect, useState } from 'react' @@ -19,7 +18,7 @@ import { useUpdateDiskAttributesMutation } from 'data/config/disk-attributes-upd import { useDiskAutoscaleCustomConfigQuery } from 'data/config/disk-autoscale-config-query' import { useUpdateDiskAutoscaleConfigMutation } from 'data/config/disk-autoscale-config-update-mutation' import { useDiskUtilizationQuery } from 'data/config/disk-utilization-query' -import { setProjectStatus } from 'data/projects/projects-query' +import { useSetProjectStatus } from 'data/projects/project-detail-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' @@ -67,10 +66,10 @@ import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' export function DiskManagementForm() { + const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() - const { ref: projectRef } = useParams() - const queryClient = useQueryClient() + const { setProjectStatus } = useSetProjectStatus() const { data: resourceWarnings } = useResourceWarningsQuery() const projectResourceWarnings = (resourceWarnings ?? [])?.find( @@ -230,7 +229,7 @@ export function DiskManagementForm() { onError: () => {}, onSuccess: () => { //Manually set project status to RESIZING, Project status should be RESIZING on next project status request. - setProjectStatus(queryClient, projectRef!, PROJECT_STATUS.RESIZING) + if (projectRef) setProjectStatus({ ref: projectRef, status: PROJECT_STATUS.RESIZING }) }, }) const { mutateAsync: updateDiskAutoscaleConfig, isLoading: isUpdatingDiskAutoscaleConfig } = diff --git a/apps/studio/components/interfaces/HomePageActions.tsx b/apps/studio/components/interfaces/HomePageActions.tsx index cfaacaf23d6a0..52ed4ea67fa2e 100644 --- a/apps/studio/components/interfaces/HomePageActions.tsx +++ b/apps/studio/components/interfaces/HomePageActions.tsx @@ -1,13 +1,13 @@ +import { useDebounce } from '@uidotdev/usehooks' import { Filter, Grid, List, Loader2, Plus, Search, X } from 'lucide-react' import Link from 'next/link' +import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs' -import { useDebounce } from '@uidotdev/usehooks' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { useOrgProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { PROJECT_STATUS } from 'lib/constants' -import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs' import { Button, Checkbox_Shadcn_, diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx index 8baf88672b236..45e5446be47af 100644 --- a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { cn } from '@ui/lib/utils' -import { Boxes, ChevronRight } from 'lucide-react' +import { ChevronRight } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' @@ -9,8 +9,13 @@ import { toast } from 'sonner' import { z } from 'zod' import { RadioGroupCard, RadioGroupCardItem } from '@ui/components/radio-group-card' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from 'components/layouts/Scaffold' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useOrganizationLinkAwsMarketplaceMutation } from 'data/organizations/organization-link-aws-marketplace-mutation' -import { useProjectsQuery } from 'data/projects/projects-query' import { DOCS_URL } from 'lib/constants' import { Organization } from 'types' import { @@ -23,19 +28,13 @@ import { Skeleton, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { - ScaffoldSection, - ScaffoldSectionContent, - ScaffoldSectionDetail, -} from '../../../layouts/Scaffold' -import { ActionCard } from '../../../ui/ActionCard' -import { ButtonTooltip } from '../../../ui/ButtonTooltip' +import { OrganizationCard } from '../OrganizationCard' import AwsMarketplaceAutoRenewalWarning from './AwsMarketplaceAutoRenewalWarning' import AwsMarketplaceOnboardingSuccessModal from './AwsMarketplaceOnboardingSuccessModal' import { CloudMarketplaceOnboardingInfo } from './cloud-marketplace-query' import NewAwsMarketplaceOrgModal from './NewAwsMarketplaceOrgModal' -interface Props { +interface AwsMarketplaceLinkExistingOrgProps { organizations?: Organization[] | undefined onboardingInfo?: CloudMarketplaceOnboardingInfo | undefined isLoadingOnboardingInfo: boolean @@ -45,13 +44,13 @@ const FormSchema = z.object({ orgSlug: z.string(), }) -export type LinkExistingOrgForm = z.infer +type LinkExistingOrgForm = z.infer -const AwsMarketplaceLinkExistingOrg = ({ +export const AwsMarketplaceLinkExistingOrg = ({ organizations, onboardingInfo, isLoadingOnboardingInfo, -}: Props) => { +}: AwsMarketplaceLinkExistingOrgProps) => { const router = useRouter() const { query: { buyer_id: buyerId }, @@ -94,9 +93,6 @@ const AwsMarketplaceLinkExistingOrg = ({ return { orgsLinkable: linkable, orgsNotLinkable: notLinkable } }, [sortedOrganizations, onboardingInfo?.organization_linking_eligibility]) - const { data } = useProjectsQuery() - const projects = data?.projects ?? [] - const [isNotLinkableOrgListOpen, setIsNotLinkableOrgListOpen] = useState(false) const [orgLinkedSuccessfully, setOrgLinkedSuccessfully] = useState(false) const [showOrgCreationDialog, setShowOrgCreationDialog] = useState(false) @@ -200,39 +196,24 @@ const AwsMarketplaceLinkExistingOrg = ({ subscription at the moment.

) : ( - <> - {orgsLinkable.map((org) => { - const numProjects = projects.filter( - (p) => p.organization_slug === org.slug - ).length - return ( - - } - title={org.name} - description={`${org.plan.name} Plan • ${numProjects > 0 ? `${numProjects} Project${numProjects > 1 ? 's' : ''}` : '0 Projects'}`} - /> - } + orgsLinkable.map((org) => ( + - ) - })} - + } + /> + )) )} )} @@ -274,20 +255,14 @@ const AwsMarketplaceLinkExistingOrg = ({ the organization to link it.

- {orgsNotLinkable.map((org) => { - const numProjects = projects.filter( - (p) => p.organization_slug === org.slug - ).length - return ( - } - title={org.name} - description={`${org.plan.name} Plan • ${numProjects > 0 ? `${numProjects} Project${numProjects > 1 ? 's' : ''}` : '0 Projects'}`} - /> - ) - })} + {orgsNotLinkable.map((org) => ( + + ))}
@@ -337,5 +312,3 @@ const AwsMarketplaceLinkExistingOrg = ({ ) } - -export default AwsMarketplaceLinkExistingOrg diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index e8c94cdb15d60..ee6fd9f90a493 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -1,15 +1,18 @@ +import { zodResolver } from '@hookform/resolvers/zod' import { Elements } from '@stripe/react-stripe-js' import type { PaymentIntentResult, PaymentMethod, StripeElementsOptions } from '@stripe/stripe-js' +import { loadStripe } from '@stripe/stripe-js' import _ from 'lodash' -import { ExternalLink, HelpCircle } from 'lucide-react' +import { HelpCircle } from 'lucide-react' +import { useTheme } from 'next-themes' import Link from 'next/link' import { useRouter } from 'next/router' -import { parseAsString, useQueryStates } from 'nuqs' +import { parseAsBoolean, parseAsString, useQueryStates } from 'nuqs' import { useEffect, useMemo, useRef, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' -import { loadStripe } from '@stripe/stripe-js' import { LOCAL_STORAGE_KEYS } from 'common' import { getStripeElementsAppearanceOptions } from 'components/interfaces/Billing/Payment/Payment.utils' import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation' @@ -26,11 +29,12 @@ import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { PRICING_TIER_LABELS_ORG, STRIPE_PUBLIC_KEY } from 'lib/constants' import { useProfile } from 'lib/profile' -import { useTheme } from 'next-themes' import { Button, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, Input_Shadcn_, - Label_Shadcn_, Select_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, @@ -42,6 +46,7 @@ import { TooltipTrigger, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { NewPaymentMethodElement, type PaymentMethodElementRef, @@ -79,7 +84,7 @@ const formSchema = z.object({ .string() .transform((val) => val.toUpperCase()) .pipe(z.enum(plans)), - name: z.string().min(1), + name: z.string().min(1, 'Organization name is required'), kind: z .string() .transform((val) => val.toUpperCase()) @@ -94,6 +99,8 @@ type FormState = z.infer const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) +const FORM_ID = 'new-org-form' + /** * No org selected yet, create a new one * [Joshen] Need to refactor to use Form_Shadcn here @@ -141,45 +148,48 @@ export const NewOrgForm = ({ [setupIntent, resolvedTheme] ) - const [formState, setFormState] = useState({ - plan: 'FREE', - name: '', - kind: ORG_KIND_DEFAULT, - size: ORG_SIZE_DEFAULT, - spend_cap: true, - }) - const [searchParams] = useQueryStates({ returnTo: parseAsString.withDefault(''), auth_id: parseAsString.withDefault(''), + token: parseAsString.withDefault(''), }) - const updateForm = (key: keyof FormState, value: unknown) => { - setFormState((prev) => ({ ...prev, [key]: value })) - } - - useEffect(() => { - if (!router.isReady) return + const [defaultValues] = useQueryStates({ + name: parseAsString.withDefault(''), + kind: parseAsString.withDefault(ORG_KIND_DEFAULT), + plan: parseAsString.withDefault('FREE'), + size: parseAsString.withDefault(ORG_SIZE_DEFAULT), + spend_cap: parseAsBoolean.withDefault(true), + }) - const { name, kind, plan, size, spend_cap } = router.query + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + plan: defaultValues.plan.toUpperCase() as (typeof plans)[number], + name: defaultValues.name, + kind: defaultValues.kind as typeof ORG_KIND_DEFAULT, + size: defaultValues.size as keyof typeof ORG_SIZE_TYPES, + spend_cap: defaultValues.spend_cap, + }, + }) - if (typeof name === 'string') updateForm('name', name) - if (typeof kind === 'string') updateForm('kind', kind) - if (typeof plan === 'string' && plans.includes(plan.toUpperCase() as (typeof plans)[number])) { - const uppercasedPlan = plan.toUpperCase() as (typeof plans)[number] - updateForm('plan', uppercasedPlan) - onPlanSelected(uppercasedPlan) - } - if (typeof size === 'string') updateForm('size', size) - if (typeof spend_cap === 'string') updateForm('spend_cap', spend_cap === 'true') - }, [router.isReady]) + useEffect(() => { + form.reset({ + plan: defaultValues.plan.toUpperCase() as (typeof plans)[number], + name: defaultValues.name, + kind: defaultValues.kind as typeof ORG_KIND_DEFAULT, + size: defaultValues.size as keyof typeof ORG_SIZE_TYPES, + spend_cap: defaultValues.spend_cap, + }) + }, [defaultValues, form]) useEffect(() => { - if (!formState.name && organizations?.length === 0 && !user.isLoading) { + const currentName = form.getValues('name') + if (!currentName && isSuccess && organizations?.length === 0 && user.isSuccess) { const prefilledOrgName = user.profile?.username ? user.profile.username + `'s Org` : 'My Org' - updateForm('name', prefilledOrgName) + form.setValue('name', prefilledOrgName) } - }, [isSuccess]) + }, [isSuccess, form, organizations?.length, user.profile?.username, user.isSuccess]) const [newOrgLoading, setNewOrgLoading] = useState(false) const [paymentMethod, setPaymentMethod] = useState() @@ -218,9 +228,9 @@ export const NewOrgForm = ({ if (paymentIntentConfirmation.paymentIntent?.status === 'succeeded') { await confirmPendingSubscriptionChange({ payment_intent_id: paymentIntentConfirmation.paymentIntent.id, - name: formState.name, - kind: formState.kind, - size: formState.size, + name: form.getValues('name'), + kind: form.getValues('kind'), + size: form.getValues('size'), }) } else { // If the payment intent is not successful, we reset the payment method and show an error @@ -237,20 +247,21 @@ export const NewOrgForm = ({ ? user.profile.username + `'s Project` : 'My Project' - if (searchParams.returnTo && searchParams.auth_id) { - router.push(`${searchParams.returnTo}?auth_id=${searchParams.auth_id}`, undefined, { - shallow: false, - }) + if (searchParams.returnTo) { + const url = new URL(searchParams.returnTo, window.location.origin) + if (searchParams.auth_id) { + url.searchParams.set('auth_id', searchParams.auth_id) + } + if (searchParams.token) { + url.searchParams.set('token', searchParams.token) + } + + router.push(url.toString(), undefined, { shallow: false }) } else { router.push(`/new/${org.slug}?projectName=${prefilledProjectName}`) } } - function validateOrgName(name: any) { - const value = name ? name.trim() : '' - return value.length >= 1 - } - const stripeOptionsConfirm = useMemo(() => { return { clientSecret: paymentIntentSecret, @@ -259,6 +270,7 @@ export const NewOrgForm = ({ }, [paymentIntentSecret, resolvedTheme]) async function createOrg( + formValues: z.infer, paymentMethodId?: string, customerData?: { address: CustomerAddress | null @@ -266,17 +278,17 @@ export const NewOrgForm = ({ tax_id: CustomerTaxId | null } ) { - const dbTier = formState.plan === 'PRO' && !formState.spend_cap ? 'PAYG' : formState.plan + const dbTier = formValues.plan === 'PRO' && !formValues.spend_cap ? 'PAYG' : formValues.plan createOrganization({ - name: formState.name, - kind: formState.kind, + name: formValues.name, + kind: formValues.kind, tier: ('tier_' + dbTier.toLowerCase()) as | 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team', - ...(formState.kind == 'COMPANY' ? { size: formState.size } : {}), + ...(formValues.kind == 'COMPANY' ? { size: formValues.size } : {}), payment_method: paymentMethodId, billing_name: dbTier === 'FREE' ? undefined : customerData?.billing_name, address: dbTier === 'FREE' ? null : customerData?.address, @@ -286,11 +298,19 @@ export const NewOrgForm = ({ const paymentRef = useRef(null) - const handleSubmit = async () => { + const onSubmit: SubmitHandler> = async (formValues) => { + debugger + const hasFreeOrgWithProjects = freeOrgs.some((it) => projectsByOrg[it.slug]?.length > 0) + + if (hasFreeOrgWithProjects && formValues.plan !== 'FREE') { + setIsOrgCreationConfirmationModalVisible(true) + return + } + setNewOrgLoading(true) - if (formState.plan === 'FREE') { - await createOrg() + if (formValues.plan === 'FREE') { + await createOrg(formValues) } else if (!paymentMethod) { const result = await paymentRef.current?.createPaymentMethod() if (result) { @@ -301,12 +321,12 @@ export const NewOrgForm = ({ tax_id: result.taxId, } - createOrg(result.paymentMethod.id, customerData) + createOrg(formValues, result.paymentMethod.id, customerData) } else { setNewOrgLoading(false) } } else { - createOrg(paymentMethod.id) + createOrg(formValues, paymentMethod.id) } } @@ -315,48 +335,34 @@ export const NewOrgForm = ({ return onPaymentMethodReset() } - const onSubmitWithOrgCreation = async (event: any) => { - event.preventDefault() - - const isOrgNameValid = validateOrgName(formState.name) - if (!isOrgNameValid) { - return toast.error('Organization name is empty') - } - - const hasFreeOrgWithProjects = freeOrgs.some((it) => projectsByOrg[it.slug]?.length > 0) - - if (hasFreeOrgWithProjects && formState.plan !== 'FREE') { - setIsOrgCreationConfirmationModalVisible(true) - } else { - await handleSubmit() - } - } - return ( -
- -

Create a new organization

-
- } - footer={ -
- -
-

- You can rename your organization later + + + +

Create a new organization

+

+ Organizations are a way to group your projects. Each organization can be configured + with different team members and billing settings.

+
+ } + footer={ +
+ +
-
- } - className="overflow-visible" - > - -

This is your organization within Supabase.

-

- For example, you can use the name of your company or department. -

-
- -
-
- Name -
-
- updateForm('name', e.target.value)} - /> -
- - What's the name of your company or team? - -
-
-
-
- -
-
- Type -
-
- updateForm('kind', value)} - > - - - - - - {Object.entries(ORG_KIND_TYPES).map(([k, v]) => ( - - {v} - - ))} - - - -
- - What would best describe your organization? - -
-
-
-
- - {formState.kind == 'COMPANY' && ( - -
-
- Company size -
-
- updateForm('size', value)} - > - - - - - - {Object.entries(ORG_SIZE_TYPES).map(([k, v]) => ( - - {v} - - ))} - - - -
- +
+ + ( + - How many people are in your company? - -
-
-
- - )} - - {isBillingEnabled && ( - -
-
- - Plan - - - - Pricing - - -
-
- { - updateForm('plan', value) - onPlanSelected(value) - }} - > - - - - - - {Object.entries(PRICING_TIER_LABELS_ORG).map(([k, v]) => ( - - {v} - - ))} - - - -
- + + + + )} + /> + + + ( + - The Plan applies to your new organization. - -
-
-
-
- )} - - {formState.plan === 'PRO' && ( - <> - -
-
- - Spend Cap - - - setShowSpendCapHelperModal(true)} - /> -
-
- updateForm('spend_cap', !formState.spend_cap)} - /> - - {formState.spend_cap - ? `Usage is limited to the plan's quota.` - : `You pay for overages beyond the plan's quota.`} - -
-
+ + + + + + + + {Object.entries(ORG_KIND_TYPES).map(([k, v]) => ( + + {v} + + ))} + + + + + )} + />
- setShowSpendCapHelperModal(false)} - /> - - )} + {form.watch('kind') == 'COMPANY' && ( + + ( + + + + + + + + + {Object.entries(ORG_SIZE_TYPES).map(([k, v]) => ( + + {v} + + ))} + + + + + )} + /> + + )} - {setupIntent && formState.plan !== 'FREE' && ( - - + {isBillingEnabled && ( - ( + + Which plan fits your organization's needs best?{' '} + + Learn more + + . + + } + > + + { + field.onChange(value) + onPlanSelected(value) + }} + > + + + + + + {Object.entries(PRICING_TIER_LABELS_ORG).map(([k, v]) => ( + + {v} + + ))} + + + + + )} /> - - - )} - - - setIsOrgCreationConfirmationModalVisible(false)} - onConfirm={async () => { - await handleSubmit() - setIsOrgCreationConfirmationModalVisible(false) - }} - variant={'warning'} - > -

- Supabase{' '} - - bills per organization - - . If you want to upgrade your existing projects, upgrade your existing organization - instead. -

- -
    - {freeOrgs - .filter((it) => projectsByOrg[it.slug]?.length > 0) - .map((org) => { - const orgProjects = projectsByOrg[org.slug].map((it) => it.name) - - return ( -
  • -
    - {org.name} -
    + } + layout="horizontal" + description={ + field.value + ? `Usage is limited to the plan's quota.` + : `You pay for overages beyond the plan's quota.` + } + > + + + + + )} + /> + + + setShowSpendCapHelperModal(false)} + /> + + )} + + {setupIntent && form.watch('plan') !== 'FREE' && ( + + + + + + )} +
+ + + setIsOrgCreationConfirmationModalVisible(false)} + onConfirm={async () => { + await onSubmit(form.getValues()) + setIsOrgCreationConfirmationModalVisible(false) + }} + variant={'warning'} + > +

+ Supabase{' '} + + bills per organization + + . If you want to upgrade your existing projects, upgrade your existing organization + instead. +

+ +
    + {freeOrgs + .filter((it) => projectsByOrg[it.slug]?.length > 0) + .map((org) => { + const orgProjects = projectsByOrg[org.slug].map((it) => it.name) + + return ( +
  • +
    +

    {org.name}

    + +
    + {orgProjects.length <= 2 ? ( + {orgProjects.join(' and ')} + ) : ( +
    + {orgProjects.slice(0, 2).join(', ')} and{' '} + + + + {orgProjects.length - 2} other{' '} + {orgProjects.length === 3 ? 'project' : 'project'} + + + +
      + {orgProjects.slice(2).map((project) => ( +
    • {project}
    • + ))} +
    +
    +
    +
    + )} +
    +
    + -
-
- {orgProjects.length <= 2 ? ( - {orgProjects.join(' and ')} - ) : ( -
- {orgProjects.slice(0, 2).join(', ')} and{' '} - - - - {orgProjects.length - 2} other{' '} - {orgProjects.length === 3 ? 'project' : 'project'} - - - -
    - {orgProjects.slice(2).map((project) => ( -
  • {project}
  • - ))} -
-
-
-
- )} -
- - ) - })} - - - - {stripePromise && paymentIntentSecret && paymentMethod && ( - - - paymentIntentConfirmed(paymentIntentConfirmation) - } - onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)} - onError={(err) => { - toast.error(err.message, { duration: 10_000 }) - setNewOrgLoading(false) - resetPaymentMethod() - }} - /> - - )} - + + ) + })} + + + + {stripePromise && paymentIntentSecret && paymentMethod && ( + + + paymentIntentConfirmed(paymentIntentConfirmation) + } + onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)} + onError={(err) => { + toast.error(err.message, { duration: 10_000 }) + setNewOrgLoading(false) + resetPaymentMethod() + }} + /> + + )} + + ) } diff --git a/apps/studio/components/interfaces/Organization/OrgNotFound.tsx b/apps/studio/components/interfaces/Organization/OrgNotFound.tsx index 2da3c4f293dc5..490a0bdc93270 100644 --- a/apps/studio/components/interfaces/Organization/OrgNotFound.tsx +++ b/apps/studio/components/interfaces/Organization/OrgNotFound.tsx @@ -29,7 +29,7 @@ export const OrgNotFound = ({ slug }: { slug?: string }) => { )} -

Select an organization to create your new project from

+

Select an organization to create your new project in

{isOrganizationsLoading && ( diff --git a/apps/studio/components/interfaces/Organization/OrganizationCard.tsx b/apps/studio/components/interfaces/Organization/OrganizationCard.tsx index f9cb258318cf5..0a4b81c256fc7 100644 --- a/apps/studio/components/interfaces/Organization/OrganizationCard.tsx +++ b/apps/studio/components/interfaces/Organization/OrganizationCard.tsx @@ -4,26 +4,36 @@ import Link from 'next/link' import { useIsMFAEnabled } from 'common' import { ActionCard } from 'components/ui/ActionCard' import { useOrgProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query' +import { Fragment } from 'react' import { Organization } from 'types' import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' export const OrganizationCard = ({ organization, href, + isLink = true, + className, }: { organization: Organization href?: string + isLink?: boolean + className?: string }) => { const isUserMFAEnabled = useIsMFAEnabled() const { data } = useOrgProjectsInfiniteQuery({ slug: organization.slug }) const numProjects = data?.pages[0].pagination.count ?? 0 const isMfaRequired = organization.organization_requires_mfa + const Parent = isLink ? Link : Fragment + return ( - + div]:w-full [&>div]:items-center')} + className={cn( + 'flex items-center min-h-[70px] [&>div]:w-full [&>div]:items-center', + className + )} icon={} title={organization.name} description={ @@ -52,6 +62,6 @@ export const OrganizationCard = ({
} /> - + ) } diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx index 2a2cd9f52dcd1..53296ab48b1ff 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx @@ -45,11 +45,15 @@ export const MemberActions = ({ member }: MemberActionsProps) => { const { data: selectedOrganization } = useSelectedOrganizationQuery() const { data: permissions } = usePermissionsQuery() - const { data } = useProjectsQuery() const { data: members } = useOrganizationMembersQuery({ slug }) + const { data: allRoles } = useOrganizationRolesV2Query({ slug }) + const hasProjectScopedRoles = (allRoles?.project_scoped_roles ?? []).length > 0 + // [Joshen] We only need this data if the org has project scoped roles + const { data } = useProjectsQuery({ enabled: hasProjectScopedRoles }) const allProjects = data?.projects ?? [] + const memberIsUser = member.gotrue_id == profile?.gotrue_id const orgScopedRoles = allRoles?.org_scoped_roles ?? [] const projectScopedRoles = allRoles?.project_scoped_roles ?? [] diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx index befed92d41f2b..236a3a9fac176 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx @@ -35,14 +35,16 @@ export const MemberRow = ({ member }: MemberRowProps) => { const { profile } = useProfile() const { data: selectedOrganization } = useSelectedOrganizationQuery() - const { data } = useProjectsQuery() - const projects = data?.projects ?? [] const { data: roles, isLoading: isLoadingRoles } = useOrganizationRolesV2Query({ slug: selectedOrganization?.slug, }) + const hasProjectScopedRoles = (roles?.project_scoped_roles ?? []).length > 0 + + // [Joshen] We only need this data if the org has project scoped roles + const { data } = useProjectsQuery({ enabled: hasProjectScopedRoles }) + const projects = data?.projects ?? [] const orgProjects = projects?.filter((p) => p.organization_id === selectedOrganization?.id) - const hasProjectScopedRoles = (roles?.project_scoped_roles ?? []).length > 0 const isInvitedUser = Boolean(member.invited_id) const isEmailUser = member.username === member.primary_email const isFlyUser = Boolean(member.primary_email?.endsWith('customer.fly.io')) diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx index a14897c219c3a..2ca9ece52dfc9 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx @@ -1,6 +1,7 @@ import { Search } from 'lucide-react' import { useState } from 'react' +import { useParams } from 'common' import { ScaffoldActionsContainer, ScaffoldActionsGroup, @@ -10,14 +11,28 @@ import { ScaffoldTitle, } from 'components/layouts/Scaffold' import { DocsButton } from 'components/ui/DocsButton' +import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' +import { useProjectsInfiniteQuery } from 'data/projects/projects-infinite-query' import { DOCS_URL } from 'lib/constants' +import { Admonition } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' import { InviteMemberButton } from './InviteMemberButton' import MembersView from './MembersView' export const TeamSettings = () => { + const { slug } = useParams() const [searchString, setSearchString] = useState('') + const { data: roles } = useOrganizationRolesV2Query({ slug }) + const hasProjectScopedRoles = (roles?.project_scoped_roles ?? []).length > 0 + + // [Joshen] Using the infinite query to get total count. We're using useProjectsInfiniteQuery + // here instead of useOrgProjectsInfiniteQuery because the UI here are using useProjectsQuery + // which returns all projects the user has access to (not scoped to org) + const { data } = useProjectsInfiniteQuery({}) + const totalCount = data?.pages[0].pagination.count ?? 0 + const threshold = 1000 + return ( Team @@ -38,6 +53,16 @@ export const TeamSettings = () => { + + {hasProjectScopedRoles && totalCount > threshold && ( + + )} + diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx index 1ba27891feb9a..e8fc5a12b5b06 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx @@ -39,8 +39,12 @@ export const UpdateRolesConfirmationModal = ({ const { slug } = useParams() const queryClient = useQueryClient() const { data: organization } = useSelectedOrganizationQuery() + const { data: allRoles } = useOrganizationRolesV2Query({ slug: organization?.slug }) - const { data } = useProjectsQuery() + const hasProjectScopedRoles = (allRoles?.project_scoped_roles ?? []).length > 0 + + // [Joshen] We only need this data if the org has project scoped roles + const { data } = useProjectsQuery({ enabled: hasProjectScopedRoles }) const projects = data?.projects ?? [] // [Joshen] Separate saving state instead of using RQ due to several successive steps diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx index 26aae79d3bfb5..64e649556b8a1 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx @@ -62,10 +62,14 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP const { data: organization } = useSelectedOrganizationQuery() const isOptedIntoProjectLevelPermissions = useHasAccessToProjectLevelPermissions(slug as string) - const { data } = useProjectsQuery() + const { data: allRoles, isSuccess: isSuccessRoles } = useOrganizationRolesV2Query({ slug }) + const hasProjectScopedRoles = (allRoles?.project_scoped_roles ?? []).length > 0 + + // [Joshen] We only need this data if the org has project scoped roles + const { data } = useProjectsQuery({ enabled: hasProjectScopedRoles }) const projects = data?.projects ?? [] + const { data: permissions } = usePermissionsQuery() - const { data: allRoles, isSuccess: isSuccessRoles } = useOrganizationRolesV2Query({ slug }) // [Joshen] We use the org scoped roles as the source for available roles const orgScopedRoles = allRoles?.org_scoped_roles ?? [] diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx index 0d9449a658062..e9efbde3744bd 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx @@ -6,7 +6,7 @@ import AlertError from 'components/ui/AlertError' import Panel from 'components/ui/Panel' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { PricingMetric } from 'data/analytics/org-daily-stats-query' -import { useOrgProjectsQuery } from 'data/projects/org-projects' +import { useOrgProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query' import type { OrgSubscription } from 'data/subscriptions/types' import { OrgUsageResponse } from 'data/usage/org-usage-query' import { PROJECT_STATUS } from 'lib/constants' @@ -31,7 +31,7 @@ export interface DiskUsageProps { currentBillingCycleSelected: boolean } -const DiskUsage = ({ +export const DiskUsage = ({ slug, projectRef, attribute, @@ -39,24 +39,15 @@ const DiskUsage = ({ usage, currentBillingCycleSelected, }: DiskUsageProps) => { - const { - data: diskUsage, - isError, - isLoading, - isSuccess, - error, - } = useOrgProjectsQuery( - { - orgSlug: slug, - }, - { - enabled: currentBillingCycleSelected, - } + const { data, isError, isLoading, isSuccess, error } = useOrgProjectsInfiniteQuery( + { slug }, + { enabled: currentBillingCycleSelected } ) + const projects = useMemo(() => data?.pages.flatMap((page) => page.projects) || [], [data?.pages]) const relevantProjects = useMemo(() => { - return diskUsage - ? diskUsage.projects + return isSuccess + ? projects .filter((project) => { // We do want to show branches that are exceeding the 8 GB limit, as people could have persistent or very long-living branches const isBranchExceedingFreeQuota = @@ -72,7 +63,8 @@ const DiskUsage = ({ }) .filter((it) => it.ref === projectRef || !projectRef) : [] - }, [diskUsage, projectRef]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess, projects, projectRef]) const hasProjectsExceedingDiskSize = useMemo(() => { return relevantProjects.some((it) => @@ -242,5 +234,3 @@ const DiskUsage = ({
) } - -export default DiskUsage diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx index e2327177cb30d..60c660052fc30 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx @@ -1,13 +1,13 @@ -import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' +import { ScaffoldContainer } from 'components/layouts/Scaffold' import { DataPoint } from 'data/analytics/constants' +import { PricingMetric } from 'data/analytics/org-daily-stats-query' import type { OrgSubscription } from 'data/subscriptions/types' import { useOrgUsageQuery } from 'data/usage/org-usage-query' import SectionHeader from '../SectionHeader' import { CategoryMetaKey, USAGE_CATEGORIES } from '../Usage.constants' import AttributeUsage from './AttributeUsage' -import DiskUsage from './DiskUsage' -import { PricingMetric } from 'data/analytics/org-daily-stats-query' import DatabaseSizeUsage from './DatabaseSizeUsage' +import { DiskUsage } from './DiskUsage' export interface ChartMeta { [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean } diff --git a/apps/studio/components/interfaces/ProjectCreation/SecurityOptions.tsx b/apps/studio/components/interfaces/ProjectCreation/SecurityOptions.tsx index 3a92bbad82789..7e13206a3a501 100644 --- a/apps/studio/components/interfaces/ProjectCreation/SecurityOptions.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/SecurityOptions.tsx @@ -147,7 +147,7 @@ export const SecurityOptions = ({ /> )}

- These settings can be changed after the project is created via the project's settings + These two security options can be changed after your project is created

) diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx b/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx index 3f8223e8a6ccf..0a063e58828c7 100644 --- a/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx +++ b/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx @@ -1,5 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useQueryClient } from '@tanstack/react-query' import { Pause } from 'lucide-react' import { useRouter } from 'next/router' import { useState } from 'react' @@ -7,8 +6,8 @@ import { toast } from 'sonner' import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useSetProjectStatus } from 'data/projects/project-detail-query' import { useProjectPauseMutation } from 'data/projects/project-pause-mutation' -import { setProjectStatus } from 'data/projects/projects-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' @@ -17,9 +16,10 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' const PauseProjectButton = () => { const router = useRouter() - const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const { data: organization } = useSelectedOrganizationQuery() + const { setProjectStatus } = useSetProjectStatus() + const isProjectActive = useIsProjectActive() const [isModalOpen, setIsModalOpen] = useState(false) @@ -36,7 +36,7 @@ const PauseProjectButton = () => { const { mutate: pauseProject, isLoading: isPausing } = useProjectPauseMutation({ onSuccess: (_, variables) => { - setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.PAUSING) + setProjectStatus({ ref: variables.ref, status: PROJECT_STATUS.PAUSING }) toast.success('Pausing project...') router.push(`/project/${projectRef}`) }, diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/ProjectUpgradeAlert.tsx b/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert.tsx similarity index 97% rename from apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/ProjectUpgradeAlert.tsx rename to apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert.tsx index c656e5773098e..3209c31a593de 100644 --- a/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/ProjectUpgradeAlert.tsx +++ b/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert.tsx @@ -1,5 +1,4 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { useQueryClient } from '@tanstack/react-query' import { AlertCircle, AlertTriangle } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' @@ -17,8 +16,8 @@ import { useProjectUpgradeEligibilityQuery, } from 'data/config/project-upgrade-eligibility-query' import { ReleaseChannel } from 'data/projects/new-project.constants' +import { useSetProjectStatus } from 'data/projects/project-detail-query' import { useProjectUpgradeMutation } from 'data/projects/project-upgrade-mutation' -import { setProjectStatus } from 'data/projects/projects-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { DOCS_URL, PROJECT_STATUS } from 'lib/constants' import { @@ -62,13 +61,13 @@ export const extractPostgresVersionDetails = (value: string): PostgresVersionDet return { postgresEngine, releaseChannel } as PostgresVersionDetails } -const ProjectUpgradeAlert = () => { +export const ProjectUpgradeAlert = () => { const router = useRouter() const { ref } = useParams() - const queryClient = useQueryClient() const { data: org } = useSelectedOrganizationQuery() - const [showUpgradeModal, setShowUpgradeModal] = useState(false) + const { setProjectStatus } = useSetProjectStatus() + const [showUpgradeModal, setShowUpgradeModal] = useState(false) const projectUpgradeDisabled = useFlag('disableProjectUpgrade') const planId = org?.plan.id ?? 'free' @@ -87,7 +86,7 @@ const ProjectUpgradeAlert = () => { const { mutate: upgradeProject, isLoading: isUpgrading } = useProjectUpgradeMutation({ onSuccess: (res, variables) => { - setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.UPGRADING) + setProjectStatus({ ref: variables.ref, status: PROJECT_STATUS.UPGRADING }) toast.success('Upgrading project') router.push(`/project/${variables.ref}?upgradeInitiated=true&trackingId=${res.tracking_id}`) }, @@ -312,5 +311,3 @@ const ProjectUpgradeAlert = () => { ) } - -export default ProjectUpgradeAlert diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/index.ts b/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/index.ts deleted file mode 100644 index b2e2a24a42eae..0000000000000 --- a/apps/studio/components/interfaces/Settings/General/Infrastructure/ProjectUpgradeAlert/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ProjectUpgradeAlert } from './ProjectUpgradeAlert' diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx b/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx index ac120c3008c51..31b7e7afbc50e 100644 --- a/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx +++ b/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx @@ -1,5 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useQueryClient } from '@tanstack/react-query' import { ChevronDown } from 'lucide-react' import { useRouter } from 'next/router' import { useState } from 'react' @@ -8,12 +7,13 @@ import { toast } from 'sonner' import { useFlag } from 'common' import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useSetProjectStatus } from 'data/projects/project-detail-query' import { useProjectRestartMutation } from 'data/projects/project-restart-mutation' import { useProjectRestartServicesMutation } from 'data/projects/project-restart-services-mutation' -import { setProjectStatus } from 'data/projects/projects-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { PROJECT_STATUS } from 'lib/constants' import { Button, DropdownMenu, @@ -26,10 +26,11 @@ import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog' const RestartServerButton = () => { const router = useRouter() - const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const isProjectActive = useIsProjectActive() const isAwsK8s = useIsAwsK8sCloudProvider() + const { setProjectStatus } = useSetProjectStatus() + const [serviceToRestart, setServiceToRestart] = useState<'project' | 'database'>() const { projectSettingsRestartProject } = useIsFeatureEnabled([ @@ -85,7 +86,7 @@ const RestartServerButton = () => { } const onRestartSuccess = () => { - setProjectStatus(queryClient, projectRef, 'RESTARTING') + setProjectStatus({ ref: projectRef, status: PROJECT_STATUS.RESTARTING }) toast.success('Restarting server...') router.push(`/project/${projectRef}`) setServiceToRestart(undefined) diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts index b07b68e0c1d8c..1309f266ec133 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -2,6 +2,7 @@ import dayjs from 'dayjs' import { DOCS_URL } from 'lib/constants' import type { DatetimeHelper, FilterTableSet, LogTemplate } from './Logs.types' +import { IS_PLATFORM } from 'common' export const LOGS_EXPLORER_DOCS_URL = `${DOCS_URL}/guides/platform/logs#querying-with-the-logs-explorer` @@ -538,29 +539,33 @@ export const FILTER_OPTIONS: FilterTableSet = { }, }, // function_edge_logs - function_edge_logs: { - status_code: { - label: 'Status', - key: 'status_code', - options: [ - { - key: 'error', - label: 'Error', - description: '500 error codes', - }, - { - key: 'success', - label: 'Success', - description: '200 codes', - }, - { - key: 'warning', - label: 'Warning', - description: '400 codes', - }, - ], - }, - }, + ...(IS_PLATFORM + ? { + function_edge_logs: { + status_code: { + label: 'Status', + key: 'status_code', + options: [ + { + key: 'error', + label: 'Error', + description: '500 error codes', + }, + { + key: 'success', + label: 'Success', + description: '200 codes', + }, + { + key: 'warning', + label: 'Warning', + description: '400 codes', + }, + ], + }, + }, + } + : {}), // function_logs function_logs: { severity: { diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts index 8f26529dc4902..b08cb3ce1f13a 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts @@ -1,6 +1,6 @@ import { useMonaco } from '@monaco-editor/react' import dayjs, { Dayjs } from 'dayjs' -import { get, isEqual } from 'lodash' +import { get } from 'lodash' import uniqBy from 'lodash/uniqBy' import { useEffect } from 'react' @@ -142,7 +142,7 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters, limit: n switch (table) { case 'edge_logs': - if (IS_PLATFORM === false) { + if (!IS_PLATFORM) { return ` -- local dev edge_logs query select id, edge_logs.timestamp, event_message, request.method, request.path, request.search, response.status_code @@ -162,7 +162,7 @@ limit ${limit}; ` case 'postgres_logs': - if (IS_PLATFORM === false) { + if (!IS_PLATFORM) { return ` select postgres_logs.timestamp, id, event_message, parsed.error_severity, parsed.detail, parsed.hint from postgres_logs @@ -196,6 +196,14 @@ limit ${limit} ` case 'function_edge_logs': + if (!IS_PLATFORM) { + return ` +select id, function_edge_logs.timestamp, event_message +from function_edge_logs +${orderBy} +limit ${limit} +` + } return `select id, ${table}.timestamp, event_message, response.status_code, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table} ${joins} ${where} @@ -216,11 +224,9 @@ limit ${limit} ` case 'pg_cron_logs': - const baseWhere = `where (parsed.application_name = 'pg_cron' OR event_message LIKE '%cron job%')` - - const pgCronWhere = where ? `${baseWhere} AND ${where.substring(6)}` : baseWhere + const pgCronWhere = where ? `${basePgCronWhere} AND ${where.substring(6)}` : basePgCronWhere - return `select identifier, postgres_logs.timestamp, id, event_message, parsed.error_severity, parsed.query + return `select id, postgres_logs.timestamp, event_message, parsed.error_severity, parsed.query from postgres_logs cross join unnest(metadata) as m cross join unnest(m.parsed) as parsed @@ -242,6 +248,7 @@ const genCrossJoinUnnests = (table: LogsTableName) => { cross join unnest(m.request) as request cross join unnest(m.response) as response` + case 'pg_cron_logs': case 'postgres_logs': return `cross join unnest(metadata) as m cross join unnest(m.parsed) as parsed` @@ -313,6 +320,7 @@ const calcChartStart = (params: Partial): [Dayjs, string] => return [its.add(-extendValue, trunc), trunc] } +const basePgCronWhere = `where ( parsed.application_name = 'pg_cron' or regexp_contains(event_message, 'cron job') )` /** * * generates log event chart query @@ -331,7 +339,7 @@ export const genChartQuery = ( // to calculate the chart, we need to query postgres logs if (table === LogsTableName.PG_CRON) { table = LogsTableName.POSTGRES - where = `where (parsed.application_name = 'pg_cron' OR event_message LIKE '%cron job%')` + where = basePgCronWhere } let joins = genCrossJoinUnnests(table) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/RLSDisableModal.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/RLSDisableModal.tsx index 114d5df7c3d2e..b9f18ae1d186c 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/RLSDisableModal.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/RLSDisableModal.tsx @@ -1,10 +1,13 @@ import { AlertOctagon, Lock, ShieldOff } from 'lucide-react' import { DocsButton } from 'components/ui/DocsButton' +import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { DOCS_URL } from 'lib/constants' import { Alert } from 'ui' export default function RLSDisableModalContent() { + const { docsRowLevelSecurityGuidePath } = useCustomContent(['docs:row_level_security_guide_path']) + return (
@@ -47,7 +50,7 @@ export default function RLSDisableModalContent() {
) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index 8c9dbacc7e58f..780093e1517c9 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -16,6 +16,7 @@ import { } from 'data/database/foreign-key-constraints-query' import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' @@ -89,6 +90,8 @@ export const TableEditor = ({ const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all']) const { mutate: sendEvent } = useSendEventMutation() + const { docsRowLevelSecurityGuidePath } = useCustomContent(['docs:row_level_security_guide_path']) + const [params, setParams] = useUrlState() useEffect(() => { if (params.create === 'table' && snap.ui.open === 'none') { @@ -335,7 +338,7 @@ export const TableEditor = ({ ) : ( @@ -353,7 +356,7 @@ export const TableEditor = ({ )} diff --git a/apps/studio/components/layouts/ProjectLayout/BuildingState.tsx b/apps/studio/components/layouts/ProjectLayout/BuildingState.tsx index e41e23e2bc09a..b12a696178fab 100644 --- a/apps/studio/components/layouts/ProjectLayout/BuildingState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/BuildingState.tsx @@ -1,4 +1,3 @@ -import { useQueryClient } from '@tanstack/react-query' import { ArrowRight, Loader2 } from 'lucide-react' import Link from 'next/link' @@ -7,9 +6,9 @@ import ClientLibrary from 'components/interfaces/Home/ClientLibrary' import { ExampleProject } from 'components/interfaces/Home/ExampleProject' import { EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants' import { DisplayApiSettings, DisplayConfigSettings } from 'components/ui/ProjectSettings' -import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' +import { useInvalidateProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query' +import { useInvalidateProjectDetailsQuery } from 'data/projects/project-detail-query' import { useProjectStatusQuery } from 'data/projects/project-status-query' -import { invalidateProjectsQuery } from 'data/projects/projects-query' import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' @@ -19,7 +18,9 @@ import { Badge, Button } from 'ui' const BuildingState = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() - const queryClient = useQueryClient() + + const { invalidateProjectsQuery } = useInvalidateProjectsInfiniteQuery() + const { invalidateProjectDetailsQuery } = useInvalidateProjectDetailsQuery() const showExamples = useIsFeatureEnabled('project_homepage:show_examples') @@ -36,8 +37,8 @@ const BuildingState = () => { }, onSuccess: async (res) => { if (res.status === PROJECT_STATUS.ACTIVE_HEALTHY) { - if (ref) invalidateProjectDetailsQuery(queryClient, ref) - invalidateProjectsQuery(queryClient) + if (ref) await invalidateProjectDetailsQuery(ref) + await invalidateProjectsQuery() } }, } diff --git a/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx b/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx index 015b64e5d2ef4..a8efdcd019efe 100644 --- a/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx @@ -1,15 +1,17 @@ -import { useQueryClient } from '@tanstack/react-query' -import { useParams } from 'common' +import { ExternalLink, Loader, Monitor, Server } from 'lucide-react' import Link from 'next/link' import { useEffect, useRef } from 'react' -import { Badge, Button } from 'ui' +import { useParams } from 'common' import ShimmerLine from 'components/ui/ShimmerLine' -import { invalidateProjectDetailsQuery, type Project } from 'data/projects/project-detail-query' -import { setProjectPostgrestStatus } from 'data/projects/projects-query' +import { + useInvalidateProjectDetailsQuery, + useSetProjectPostgrestStatus, + type Project, +} from 'data/projects/project-detail-query' import { DOCS_URL } from 'lib/constants' import pingPostgrest from 'lib/pingPostgrest' -import { ExternalLink, Loader, Monitor, Server } from 'lucide-react' +import { Badge, Button } from 'ui' export interface ConnectingStateProps { project: Project @@ -17,9 +19,11 @@ export interface ConnectingStateProps { const ConnectingState = ({ project }: ConnectingStateProps) => { const { ref } = useParams() - const queryClient = useQueryClient() const checkProjectConnectionIntervalRef = useRef() + const { setProjectPostgrestStatus } = useSetProjectPostgrestStatus() + const { invalidateProjectDetailsQuery } = useInvalidateProjectDetailsQuery() + useEffect(() => { if (!project.restUrl) return @@ -35,8 +39,8 @@ const ConnectingState = ({ project }: ConnectingStateProps) => { const result = await pingPostgrest(project.ref) if (result) { clearInterval(checkProjectConnectionIntervalRef.current) - setProjectPostgrestStatus(queryClient, project.ref, 'ONLINE') - await invalidateProjectDetailsQuery(queryClient, project.ref) + setProjectPostgrestStatus(project.ref, 'ONLINE') + await invalidateProjectDetailsQuery(project.ref) } } diff --git a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx index 88db43e166bbf..41fa6ea86647b 100644 --- a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx @@ -1,6 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { ExternalLink, PauseCircle } from 'lucide-react' import Link from 'next/link' @@ -15,9 +14,9 @@ import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { PostgresEngine, ReleaseChannel } from 'data/projects/new-project.constants' +import { useSetProjectStatus } from 'data/projects/project-detail-query' import { useProjectPauseStatusQuery } from 'data/projects/project-pause-status-query' import { useProjectRestoreMutation } from 'data/projects/project-restore-mutation' -import { setProjectStatus } from 'data/projects/projects-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' @@ -53,9 +52,10 @@ export const extractPostgresVersionDetails = (value: string): PostgresVersionDet export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { const { ref } = useParams() - const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const { data: selectedOrganization } = useSelectedOrganizationQuery() + const { setProjectStatus } = useSetProjectStatus() + const showPostgresVersionSelector = useFlag('showPostgresVersionSelector') const enableProBenefitWording = usePHFlag('proBenefitWording') @@ -89,7 +89,7 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { const { mutate: restoreProject, isLoading: isRestoring } = useProjectRestoreMutation({ onSuccess: (_, variables) => { - setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.RESTORING) + setProjectStatus({ ref: variables.ref, status: PROJECT_STATUS.RESTORING }) toast.success('Restoring project') }, }) diff --git a/apps/studio/components/layouts/ProjectLayout/PausingState.tsx b/apps/studio/components/layouts/ProjectLayout/PausingState.tsx index aff41b06ba188..8da3e98a85503 100644 --- a/apps/studio/components/layouts/ProjectLayout/PausingState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PausingState.tsx @@ -1,10 +1,9 @@ -import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { Badge } from 'ui' -import { Project, invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' +import { useInvalidateProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query' +import { Project, useInvalidateProjectDetailsQuery } from 'data/projects/project-detail-query' import { useProjectStatusQuery } from 'data/projects/project-status-query' -import { invalidateProjectsQuery } from 'data/projects/projects-query' import { PROJECT_STATUS } from 'lib/constants' import { Circle, Loader } from 'lucide-react' import { useEffect, useState } from 'react' @@ -15,9 +14,11 @@ export interface PausingStateProps { const PausingState = ({ project }: PausingStateProps) => { const { ref } = useParams() - const queryClient = useQueryClient() const [startPolling, setStartPolling] = useState(false) + const { invalidateProjectsQuery } = useInvalidateProjectsInfiniteQuery() + const { invalidateProjectDetailsQuery } = useInvalidateProjectDetailsQuery() + useProjectStatusQuery( { projectRef: ref }, { @@ -27,8 +28,8 @@ const PausingState = ({ project }: PausingStateProps) => { }, onSuccess: async (res) => { if (res.status === PROJECT_STATUS.INACTIVE) { - if (ref) await invalidateProjectDetailsQuery(queryClient, ref) - await invalidateProjectsQuery(queryClient) + if (ref) await invalidateProjectDetailsQuery(ref) + await invalidateProjectsQuery() } }, } diff --git a/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx b/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx index 78fdfd0f892a4..0cd471fe2105f 100644 --- a/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx @@ -1,4 +1,3 @@ -import { useQueryClient } from '@tanstack/react-query' import { CheckCircle, Download, Loader } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' @@ -7,7 +6,7 @@ import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useBackupDownloadMutation } from 'data/database/backup-download-mutation' import { useDownloadableBackupQuery } from 'data/database/backup-query' -import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' +import { useInvalidateProjectDetailsQuery } from 'data/projects/project-detail-query' import { useProjectStatusQuery } from 'data/projects/project-status-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' @@ -15,7 +14,6 @@ import { Button } from 'ui' const RestoringState = () => { const { ref } = useParams() - const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const [loading, setLoading] = useState(false) @@ -25,6 +23,8 @@ const RestoringState = () => { const backups = data?.backups ?? [] const logicalBackups = backups.filter((b) => !b.isPhysicalBackup) + const { invalidateProjectDetailsQuery } = useInvalidateProjectDetailsQuery() + useProjectStatusQuery( { projectRef: ref }, { @@ -36,7 +36,7 @@ const RestoringState = () => { if (res.status === PROJECT_STATUS.ACTIVE_HEALTHY) { setIsCompleted(true) } else { - if (ref) invalidateProjectDetailsQuery(queryClient, ref) + if (ref) invalidateProjectDetailsQuery(ref) } }, } @@ -66,7 +66,7 @@ const RestoringState = () => { if (!project) return console.error('Project is required') setLoading(true) - if (ref) await invalidateProjectDetailsQuery(queryClient, ref) + if (ref) await invalidateProjectDetailsQuery(ref) } return ( diff --git a/apps/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx b/apps/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx index 199e42864e79f..f549c242e9363 100644 --- a/apps/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx @@ -1,5 +1,4 @@ import { DatabaseUpgradeProgress, DatabaseUpgradeStatus } from '@supabase/shared-types/out/events' -import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { AlertCircle, @@ -17,7 +16,7 @@ import { useState } from 'react' import { useParams } from 'common' import { useProjectUpgradingStatusQuery } from 'data/config/project-upgrade-status-query' -import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' +import { useInvalidateProjectDetailsQuery } from 'data/projects/project-detail-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { IS_PLATFORM } from 'lib/constants' import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' @@ -26,10 +25,12 @@ import { DATABASE_UPGRADE_MESSAGES } from './UpgradingState.constants' const UpgradingState = () => { const { ref } = useParams() const queryParams = useSearchParams() - const queryClient = useQueryClient() const { data: project } = useSelectedProjectQuery() const [loading, setLoading] = useState(false) const [isExpanded, setIsExpanded] = useState(false) + + const { invalidateProjectDetailsQuery } = useInvalidateProjectDetailsQuery() + const { data } = useProjectUpgradingStatusQuery( { projectRef: ref, @@ -61,7 +62,7 @@ const UpgradingState = () => { const refetchProjectDetails = async () => { setLoading(true) - if (ref) await invalidateProjectDetailsQuery(queryClient, ref) + if (ref) await invalidateProjectDetailsQuery(ref) } const subject = 'Upgrade%20failed%20for%20project' diff --git a/apps/studio/components/ui/Panel.tsx b/apps/studio/components/ui/Panel.tsx index bf802d991b14b..dfe3ad4cc682b 100644 --- a/apps/studio/components/ui/Panel.tsx +++ b/apps/studio/components/ui/Panel.tsx @@ -13,6 +13,7 @@ interface PanelProps { wrapWithLoading?: boolean noHideOverflow?: boolean titleClasses?: string + footerClasses?: string } /** @@ -41,7 +42,7 @@ function Panel(props: PropsWithChildren) {
)} {props.children} - {props.footer &&
{props.footer}
} + {props.footer &&
{props.footer}
}
) @@ -56,9 +57,9 @@ function Content({ children, className }: { children: ReactNode; className?: str return
{children}
} -function Footer({ children }: { children: ReactNode; className?: string }) { +function Footer({ children, className }: { children: ReactNode; className?: string }) { return ( -
+
{children}
) diff --git a/apps/studio/components/ui/PasswordStrengthBar.tsx b/apps/studio/components/ui/PasswordStrengthBar.tsx index 075364cad8618..90a5ae9c937e7 100644 --- a/apps/studio/components/ui/PasswordStrengthBar.tsx +++ b/apps/studio/components/ui/PasswordStrengthBar.tsx @@ -39,7 +39,7 @@ const PasswordStrengthBar = ({ ? passwordStrengthMessage : 'This is the password to your Postgres database, so it must be strong and hard to guess.'}{' '} Generate a password diff --git a/apps/studio/data/organization-members/organization-invitation-accept-mutation.ts b/apps/studio/data/organization-members/organization-invitation-accept-mutation.ts index 82f2a54c7e73e..f482b2ce09957 100644 --- a/apps/studio/data/organization-members/organization-invitation-accept-mutation.ts +++ b/apps/studio/data/organization-members/organization-invitation-accept-mutation.ts @@ -3,7 +3,7 @@ import { toast } from 'sonner' import { handleError, post } from 'data/fetchers' import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' -import { invalidateProjectsQuery } from 'data/projects/projects-query' +import { useInvalidateProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query' import type { ResponseError } from 'types' export type OrganizationAcceptInvitationVariables = { @@ -38,6 +38,7 @@ export const useOrganizationAcceptInvitationMutation = ({ 'mutationFn' > = {}) => { const queryClient = useQueryClient() + const { invalidateProjectsQuery } = useInvalidateProjectsInfiniteQuery() return useMutation< OrganizationMemberUpdateData, @@ -46,7 +47,7 @@ export const useOrganizationAcceptInvitationMutation = ({ >((vars) => acceptOrganizationInvitation(vars), { async onSuccess(data, variables, context) { await invalidateOrganizationsQuery(queryClient) - await invalidateProjectsQuery(queryClient) + await invalidateProjectsQuery() await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/projects/keys.ts b/apps/studio/data/projects/keys.ts index 8b813097810ed..79d443d39f987 100644 --- a/apps/studio/data/projects/keys.ts +++ b/apps/studio/data/projects/keys.ts @@ -1,10 +1,12 @@ +export const INFINITE_PROJECTS_KEY_PREFIX = 'all-projects-infinite' + export const projectKeys = { list: () => ['all-projects'] as const, infiniteList: (params?: { limit: number sort?: 'name_asc' | 'name_desc' | 'created_asc' | 'created_desc' search?: string - }) => ['all-projects-infinite', params].filter(Boolean), + }) => [INFINITE_PROJECTS_KEY_PREFIX, params].filter(Boolean), infiniteListByOrg: ( slug: string | undefined, params?: { @@ -13,7 +15,7 @@ export const projectKeys = { search?: string statuses?: string[] } - ) => ['all-projects-infinite', slug, params].filter(Boolean), + ) => [INFINITE_PROJECTS_KEY_PREFIX, slug, params].filter(Boolean), status: (projectRef: string | undefined) => ['project', projectRef, 'status'] as const, types: (projectRef: string | undefined) => ['project', projectRef, 'types'] as const, detail: (projectRef: string | undefined) => ['project', projectRef, 'detail'] as const, diff --git a/apps/studio/data/projects/org-projects-infinite-query.ts b/apps/studio/data/projects/org-projects-infinite-query.ts index 69e7e24e592fe..c7d02b0bc39c3 100644 --- a/apps/studio/data/projects/org-projects-infinite-query.ts +++ b/apps/studio/data/projects/org-projects-infinite-query.ts @@ -1,10 +1,10 @@ -import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query' +import { useInfiniteQuery, UseInfiniteQueryOptions, useQueryClient } from '@tanstack/react-query' import { components } from 'api-types' import { get, handleError } from 'data/fetchers' import { useProfile } from 'lib/profile' import { ResponseError } from 'types' -import { projectKeys } from './keys' +import { INFINITE_PROJECTS_KEY_PREFIX, projectKeys } from './keys' // [Joshen] Try to keep this value a multiple of 6 (common denominator of 2 and 3) to fit the cards view // So that the last row will always be a full row of cards while there's a next page @@ -20,7 +20,8 @@ interface GetOrgProjectsInfiniteVariables { statuses?: string[] } -export type OrgProject = components['schemas']['OrganizationProjectsResponse']['projects'][number] +export type OrgProjectsResponse = components['schemas']['OrganizationProjectsResponse'] +export type OrgProject = OrgProjectsResponse['projects'][number] async function getOrganizationProjects( { @@ -73,6 +74,7 @@ export const useOrgProjectsInfiniteQuery = ( getOrganizationProjects({ slug, limit, page: pageParam, sort, search, statuses }, signal), { enabled: enabled && profile !== undefined && typeof slug !== 'undefined', + staleTime: 30 * 60 * 1000, // 30 minutes getNextPageParam(lastPage, pages) { const page = pages.length const currentTotalCount = page * limit @@ -90,3 +92,14 @@ export const getComputeSize = (project: OrgProject) => { const primaryDatabase = project.databases.find((db) => db.identifier === project.ref) return primaryDatabase?.infra_compute_size } + +export const useInvalidateProjectsInfiniteQuery = () => { + const queryClient = useQueryClient() + const invalidateProjectsQuery = () => { + // [Joshen] Temporarily for completeness while we still have UIs depending on the old endpoint (Org teams) + // Can be removed once we completely deprecate projects-query (Old unpaginated endpoint) + queryClient.invalidateQueries(projectKeys.list()) + return queryClient.invalidateQueries([INFINITE_PROJECTS_KEY_PREFIX]) + } + return { invalidateProjectsQuery } +} diff --git a/apps/studio/data/projects/org-projects.ts b/apps/studio/data/projects/org-projects.ts deleted file mode 100644 index f4284e9a45c3c..0000000000000 --- a/apps/studio/data/projects/org-projects.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query' - -import type { components } from 'data/api' -import { get, handleError } from 'data/fetchers' -import type { ResponseError } from 'types' -import { projectKeys } from './keys' - -export type OrgProjectsVariables = { - orgSlug?: string -} - -export type OrgProjectsResponse = components['schemas']['OrganizationProjectsResponse'] - -export async function getOrgProjects( - { orgSlug }: OrgProjectsVariables, - signal?: AbortSignal -): Promise { - if (!orgSlug) throw new Error('orgSlug is required') - const { data, error } = await get(`/platform/organizations/{slug}/projects`, { - params: { - path: { slug: orgSlug }, - }, - signal, - }) - if (error) handleError(error) - return data -} - -export type OrgProjectsData = Awaited> -export type OrgProjectsError = ResponseError - -export const useOrgProjectsQuery = ( - { orgSlug }: OrgProjectsVariables, - { enabled = true, ...options }: UseQueryOptions = {} -) => - useQuery( - projectKeys.orgProjects(orgSlug), - ({ signal }) => getOrgProjects({ orgSlug }, signal), - { - enabled: enabled && typeof orgSlug !== 'undefined', - ...options, - } - ) diff --git a/apps/studio/data/projects/project-detail-query.ts b/apps/studio/data/projects/project-detail-query.ts index c002b77ee3480..ac03ea6b0f70e 100644 --- a/apps/studio/data/projects/project-detail-query.ts +++ b/apps/studio/data/projects/project-detail-query.ts @@ -1,15 +1,15 @@ -import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' +import { QueryClient, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' import type { components } from 'data/api' import { get, handleError, isValidConnString } from 'data/fetchers' import type { ResponseError } from 'types' import { projectKeys } from './keys' +import { OrgProjectsResponse } from './org-projects-infinite-query' -export type ProjectDetailVariables = { ref?: string } +type ProjectDetailVariables = { ref?: string } +type PaginatedProjectsResponse = components['schemas']['ListProjectsPaginatedResponse'] -export type ProjectMinimal = components['schemas']['ProjectInfo'] export type ProjectDetail = components['schemas']['ProjectDetailResponse'] - export interface Project extends Omit { /** * postgrestStatus is available on client side only. @@ -17,7 +17,7 @@ export interface Project extends Omit { * If not we will show ConnectingState and run a polling until it's back online */ postgrestStatus?: 'ONLINE' | 'OFFLINE' - status: components['schemas']['ProjectDetailResponse']['status'] + status: ProjectDetail['status'] } export async function getProjectDetail( @@ -69,12 +69,123 @@ export const useProjectDetailQuery = ( } ) -export function invalidateProjectDetailsQuery(client: QueryClient, ref: string) { - return client.invalidateQueries(projectKeys.detail(ref)) -} - export function prefetchProjectDetail(client: QueryClient, { ref }: ProjectDetailVariables) { return client.fetchQuery(projectKeys.detail(ref), ({ signal }) => getProjectDetail({ ref }, signal) ) } + +export const useInvalidateProjectDetailsQuery = () => { + const queryClient = useQueryClient() + + const invalidateProjectDetailsQuery = (ref: string) => { + return queryClient.invalidateQueries(projectKeys.detail(ref)) + } + + return { invalidateProjectDetailsQuery } +} + +export const useSetProjectPostgrestStatus = () => { + const queryClient = useQueryClient() + + const setProjectPostgrestStatus = (ref: Project['ref'], status: Project['postgrestStatus']) => { + return queryClient.setQueriesData( + projectKeys.detail(ref), + (old) => { + if (!old) return old + return { ...old, postgrestStatus: status } + }, + { updatedAt: Date.now() } + ) + } + + return { setProjectPostgrestStatus } +} + +export const useSetProjectStatus = () => { + const queryClient = useQueryClient() + + const setProjectStatus = ({ + ref, + slug, + status, + }: { + ref: Project['ref'] + slug?: string + status: Project['status'] + }) => { + // Org projects infinite query + if (slug) { + queryClient.setQueriesData<{ pageParams: any; pages: OrgProjectsResponse[] } | undefined>( + projectKeys.infiniteListByOrg(slug), + (old) => { + if (!old) return old + return { + ...old, + pages: old.pages.map((page) => { + return { + ...page, + projects: page.projects.map((project) => + project.ref === ref ? { ...project, status } : project + ), + } + }), + } + }, + { updatedAt: Date.now() } + ) + } + + // Projects infinite query + queryClient.setQueriesData<{ pageParams: any; pages: OrgProjectsResponse[] } | undefined>( + projectKeys.infiniteList(), + (old) => { + if (!old) return old + return { + ...old, + pages: old.pages.map((page) => { + return { + ...page, + projects: page.projects.map((project) => + project.ref === ref ? { ...project, status } : project + ), + } + }), + } + }, + { updatedAt: Date.now() } + ) + + // Project details query + queryClient.setQueriesData( + projectKeys.detail(ref), + (old) => { + if (!old) return old + return { ...old, status } + }, + { updatedAt: Date.now() } + ) + + // [Joshen] Temporarily for completeness while we still have UIs depending on the old endpoint (Org teams) + // Can be removed once we completely deprecate projects-query (Old unpaginated endpoint) + queryClient.setQueriesData( + projectKeys.list(), + (old) => { + if (!old) return old + + return { + ...old, + projects: old.projects.map((project) => { + if (project.ref === ref) { + return { ...project, status } + } + return project + }), + } + }, + { updatedAt: Date.now() } + ) + } + + return { setProjectStatus } +} diff --git a/apps/studio/data/projects/projects-infinite-query.ts b/apps/studio/data/projects/projects-infinite-query.ts index bc3e9bb6f475f..2abfd883cb14a 100644 --- a/apps/studio/data/projects/projects-infinite-query.ts +++ b/apps/studio/data/projects/projects-infinite-query.ts @@ -58,6 +58,7 @@ export const useProjectsInfiniteQuery = ( ({ signal, pageParam }) => getProjects({ limit, page: pageParam, sort, search }, signal), { enabled: enabled && profile !== undefined, + staleTime: 30 * 60 * 1000, // 30 minutes getNextPageParam(lastPage, pages) { const page = pages.length const currentTotalCount = page * limit diff --git a/apps/studio/data/projects/projects-query.ts b/apps/studio/data/projects/projects-query.ts index 2553f260bf6e6..8c1c11f911116 100644 --- a/apps/studio/data/projects/projects-query.ts +++ b/apps/studio/data/projects/projects-query.ts @@ -1,20 +1,15 @@ -import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' import type { components } from 'data/api' import { get, handleError } from 'data/fetchers' import { useProfile } from 'lib/profile' import type { ResponseError } from 'types' import { projectKeys } from './keys' -import type { Project } from './project-detail-query' - -export type ProjectsVariables = { - ref?: string -} type PaginatedProjectsResponse = components['schemas']['ListProjectsPaginatedResponse'] export type ProjectInfo = PaginatedProjectsResponse['projects'][number] -export async function getProjects({ +async function getProjects({ signal, headers, }: { @@ -49,57 +44,3 @@ export const useProjectsQuery = ({ } ) } - -export function invalidateProjectsQuery(client: QueryClient) { - return client.invalidateQueries(projectKeys.list()) -} - -export function setProjectStatus( - client: QueryClient, - projectRef: Project['ref'], - status: Project['status'] -) { - client.setQueriesData( - projectKeys.list(), - (old) => { - if (!old) return old - - return { - ...old, - projects: old.projects.map((project) => { - if (project.ref === projectRef) { - return { ...project, status } - } - return project - }), - } - }, - { updatedAt: Date.now() } - ) - - client.setQueriesData( - projectKeys.detail(projectRef), - (old) => { - if (!old) return old - - return { ...old, status } - }, - { updatedAt: Date.now() } - ) -} - -export function setProjectPostgrestStatus( - client: QueryClient, - projectRef: Project['ref'], - status: Project['postgrestStatus'] -) { - client.setQueriesData( - projectKeys.detail(projectRef), - (old) => { - if (!old) return old - - return { ...old, postgrestStatus: status } - }, - { updatedAt: Date.now() } - ) -} diff --git a/apps/studio/hooks/custom-content/CustomContent.types.ts b/apps/studio/hooks/custom-content/CustomContent.types.ts index 4aa8b0216c8d0..b62e124743d80 100644 --- a/apps/studio/hooks/custom-content/CustomContent.types.ts +++ b/apps/studio/hooks/custom-content/CustomContent.types.ts @@ -5,6 +5,8 @@ export type CustomContentTypes = { dashboardAuthCustomProvider: string + docsRowLevelSecurityGuidePath: string + organizationLegalDocuments: { id: string name: string diff --git a/apps/studio/hooks/custom-content/custom-content.json b/apps/studio/hooks/custom-content/custom-content.json index 10dfa88265fbd..717ecb5852b0f 100644 --- a/apps/studio/hooks/custom-content/custom-content.json +++ b/apps/studio/hooks/custom-content/custom-content.json @@ -5,6 +5,8 @@ "dashboard_auth:custom_provider": null, + "docs:row_level_security_guide_path": "/guides/auth/row-level-security", + "organization:legal_documents": null, "project_homepage:client_libraries": [ diff --git a/apps/studio/hooks/custom-content/custom-content.sample.json b/apps/studio/hooks/custom-content/custom-content.sample.json index 943d54512736c..6cc90b3f621cc 100644 --- a/apps/studio/hooks/custom-content/custom-content.sample.json +++ b/apps/studio/hooks/custom-content/custom-content.sample.json @@ -5,6 +5,8 @@ "dashboard_auth:custom_provider": "Nimbus", + "docs:row_level_security_guide_path": "/guides/database/postgres/row-level-security", + "organization:legal_documents": [ { "id": "doc1", diff --git a/apps/studio/hooks/custom-content/custom-content.schema.json b/apps/studio/hooks/custom-content/custom-content.schema.json index 208c77c01fdea..f6f80dd2722a6 100644 --- a/apps/studio/hooks/custom-content/custom-content.schema.json +++ b/apps/studio/hooks/custom-content/custom-content.schema.json @@ -16,6 +16,11 @@ "description": "Show a custom provider on the sign in page (Continue with X)" }, + "docs:row_level_security_guide_path": { + "type": ["string"], + "description": "The path to the row level security guide in the docs" + }, + "organization:legal_documents": { "type": ["array", "null"], "description": "Renders a provided set of documents under the organization legal documents page", @@ -91,6 +96,7 @@ }, "required": [ "app:title", + "docs:row_level_security_guide_path", "organization:legal_documents", "project_homepage:client_libraries", "project_homepage:example_projects", diff --git a/apps/studio/pages/aws-marketplace-onboarding.tsx b/apps/studio/pages/aws-marketplace-onboarding.tsx index 0faea387ebf4a..ee06af6d5a980 100644 --- a/apps/studio/pages/aws-marketplace-onboarding.tsx +++ b/apps/studio/pages/aws-marketplace-onboarding.tsx @@ -1,17 +1,17 @@ -import { NextPageWithLayout } from '../types' +import { useRouter } from 'next/router' +import AwsMarketplaceCreateNewOrg from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceCreateNewOrg' +import { AwsMarketplaceLinkExistingOrg } from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg' +import AwsMarketplaceOnboardingPlaceholder from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder' +import { useCloudMarketplaceOnboardingInfoQuery } from '../components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query' +import LinkAwsMarketplaceLayout from '../components/layouts/LinkAwsMarketplaceLayout' import { ScaffoldContainer, ScaffoldDivider, ScaffoldHeader, ScaffoldTitle, } from '../components/layouts/Scaffold' -import LinkAwsMarketplaceLayout from '../components/layouts/LinkAwsMarketplaceLayout' import { useOrganizationsQuery } from '../data/organizations/organizations-query' -import AwsMarketplaceLinkExistingOrg from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg' -import AwsMarketplaceCreateNewOrg from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceCreateNewOrg' -import { useCloudMarketplaceOnboardingInfoQuery } from '../components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query' -import { useRouter } from 'next/router' -import AwsMarketplaceOnboardingPlaceholder from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder' +import { NextPageWithLayout } from '../types' const AwsMarketplaceOnboarding: NextPageWithLayout = () => { const { diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 0503384fbd90f..76f66587a5868 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -487,11 +487,9 @@ const Wizard: NextPageWithLayout = () => { title={

Create a new project

-

- Your project will have its own dedicated instance and full Postgres database. -
- An API will be set up so you can easily interact with your new database. -
+

+ Your project will have its own dedicated instance and full Postgres database. An API + will be set up so you can easily interact with your new database.

} @@ -757,38 +755,30 @@ const Wizard: NextPageWithLayout = () => { render={({ field }) => ( - Compute size - -
- +

+ The size for your dedicated database. You can change this later. + Learn more about{' '} + -

-

Compute add-ons

- -
- - {' '} + and{' '} + -
-

Compute billing

- -
- -
-
- } - description={ - <> -

- The size for your dedicated database. You can change this later. + compute billing + + .

} diff --git a/apps/studio/public/img/auth-overview/auth-overview-assistant-light.jpg b/apps/studio/public/img/auth-overview/auth-overview-assistant-light.jpg new file mode 100644 index 0000000000000..06a79b4b59390 Binary files /dev/null and b/apps/studio/public/img/auth-overview/auth-overview-assistant-light.jpg differ diff --git a/apps/studio/public/img/auth-overview/auth-overview-assistant.jpg b/apps/studio/public/img/auth-overview/auth-overview-assistant.jpg index 9a17a8c9fb653..4695c58a3e1bd 100644 Binary files a/apps/studio/public/img/auth-overview/auth-overview-assistant.jpg and b/apps/studio/public/img/auth-overview/auth-overview-assistant.jpg differ diff --git a/apps/studio/public/img/auth-overview/auth-overview-docs-light.jpg b/apps/studio/public/img/auth-overview/auth-overview-docs-light.jpg new file mode 100644 index 0000000000000..d1fd3561533e9 Binary files /dev/null and b/apps/studio/public/img/auth-overview/auth-overview-docs-light.jpg differ diff --git a/apps/studio/public/img/auth-overview/auth-overview-docs.jpg b/apps/studio/public/img/auth-overview/auth-overview-docs.jpg index 788efba5a1f94..c594eea477302 100644 Binary files a/apps/studio/public/img/auth-overview/auth-overview-docs.jpg and b/apps/studio/public/img/auth-overview/auth-overview-docs.jpg differ diff --git a/apps/studio/public/img/auth-overview/auth-overview-logs-light.jpg b/apps/studio/public/img/auth-overview/auth-overview-logs-light.jpg new file mode 100644 index 0000000000000..d91d70c07773e Binary files /dev/null and b/apps/studio/public/img/auth-overview/auth-overview-logs-light.jpg differ diff --git a/apps/studio/public/img/auth-overview/auth-overview-logs.jpg b/apps/studio/public/img/auth-overview/auth-overview-logs.jpg index 3573468e4525c..a90c9dd1901f1 100644 Binary files a/apps/studio/public/img/auth-overview/auth-overview-logs.jpg and b/apps/studio/public/img/auth-overview/auth-overview-logs.jpg differ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9f484246be6e7..0f470f2fa30e0 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -343,7 +343,7 @@ services: analytics: container_name: supabase-analytics - image: supabase/logflare:1.22.4 + image: supabase/logflare:1.22.6 restart: unless-stopped ports: - 4000:4000 diff --git a/docker/volumes/logs/vector.yml b/docker/volumes/logs/vector.yml index 1c438a8ecddb8..3493e9b3f8e55 100644 --- a/docker/volumes/logs/vector.yml +++ b/docker/volumes/logs/vector.yml @@ -35,7 +35,7 @@ transforms: rest: '.appname == "supabase-rest"' realtime: '.appname == "supabase-realtime"' storage: '.appname == "supabase-storage"' - functions: '.appname == "supabase-functions"' + functions: '.appname == "supabase-edge-functions"' db: '.appname == "supabase-db"' # Ignores non nginx errors since they are related with kong booting up kong_logs: @@ -117,6 +117,13 @@ transforms: .event_message = parsed.msg .metadata.level = parsed.level } + # Function logs are unstructured messages on stderr + functions_logs: + type: remap + inputs: + - router.functions + source: |- + .metadata.project_ref = del(.project) # Storage logs may contain json objects so we parse them for completeness storage_logs: type: remap @@ -210,7 +217,7 @@ sinks: logflare_functions: type: 'http' inputs: - - router.functions + - functions_logs encoding: codec: 'json' method: 'post' diff --git a/scripts/generateLocalEnv.js b/scripts/generateLocalEnv.js index d4134e5d53946..a09b81a161ff1 100644 --- a/scripts/generateLocalEnv.js +++ b/scripts/generateLocalEnv.js @@ -23,8 +23,8 @@ const defaultEnv = { STUDIO_PG_META_URL: '$API_URL/pg', SUPABASE_PUBLIC_URL: '$API_URL', SENTRY_IGNORE_API_RESOLUTION_ERROR: '1', - LOGFLARE_URL: 'http://localhost:54329', - LOGFLARE_API_KEY: 'api-key', + LOGFLARE_URL: 'http://127.0.0.1:54327', + LOGFLARE_PRIVATE_ACCESS_TOKEN: 'api-key', NEXT_PUBLIC_SITE_URL: 'http://localhost:8082', NEXT_PUBLIC_GOTRUE_URL: '$SUPABASE_PUBLIC_URL/auth/v1', NEXT_PUBLIC_HCAPTCHA_SITE_KEY: '10000000-ffff-ffff-ffff-000000000001',