diff --git a/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index 12bcbc13e2a..751cc11f2f3 100644 --- a/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -1,15 +1,17 @@ +import { useQueryClient } from '@tanstack/react-query' +import { useRouter } from 'next/router' import { useState } from 'react' import { Button, IconEdit2, IconInfo, Input, Listbox } from 'ui' -import { useRouter } from 'next/router' -import { API_URL, BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants' +import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js' +import type { PaymentMethod } from '@stripe/stripe-js' +import InformationBox from 'components/ui/InformationBox' +import Panel from 'components/ui/Panel' +import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' import { useStore } from 'hooks' import { post } from 'lib/common/fetch' -import Panel from 'components/ui/Panel' -import { useElements, useStripe, PaymentElement } from '@stripe/react-stripe-js' -import type { PaymentMethod } from '@stripe/stripe-js' +import { API_URL, BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants' import { getURL } from 'lib/helpers' -import InformationBox from 'components/ui/InformationBox' const ORG_KIND_TYPES = { PERSONAL: 'Personal', @@ -38,7 +40,8 @@ interface NewOrgFormProps { * No org selected yet, create a new one */ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { - const { ui, app } = useStore() + const queryClient = useQueryClient() + const { ui } = useStore() const router = useRouter() const stripe = useStripe() const elements = useElements() @@ -97,7 +100,7 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { }) } else { const org = response - app.onOrgAdded(org) + await invalidateOrganizationsQuery(queryClient) router.push(`/new/${org.slug}`) } setNewOrgLoading(false) diff --git a/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx b/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx index c3d88b9915f..098c514f2f7 100644 --- a/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx +++ b/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx @@ -13,6 +13,7 @@ import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants' import Panel from 'components/ui/Panel' import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' +import { getProjectDetail } from 'data/projects/project-detail-query' const ResetDbPassword: FC = ({ disabled = false }) => { const { ui, app, meta } = useStore() @@ -65,7 +66,11 @@ const ResetDbPassword: FC = ({ disabled = false }) => { setIsUpdatingPassword(true) const res = await patch(`${API_URL}/projects/${ref}/db-password`, { password }) if (!res.error) { - await app.projects.fetchDetail(ref, (project) => meta.setProjectDetails(project)) + const project = await getProjectDetail({ ref }) + if (project) { + meta.setProjectDetails(project) + } + ui.setNotification({ category: 'success', message: res.message }) setShowResetDbPass(false) } else { diff --git a/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx b/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx index 449c6f1fb13..281dea73fa2 100644 --- a/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx +++ b/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx @@ -1,14 +1,17 @@ -import { FC, useState } from 'react' -import { useRouter } from 'next/router' -import { observer } from 'mobx-react-lite' import * as Tooltip from '@radix-ui/react-tooltip' -import { Button, IconPause } from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useQueryClient } from '@tanstack/react-query' +import { observer } from 'mobx-react-lite' +import { useRouter } from 'next/router' +import { FC, useState } from 'react' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' +import { invalidateProjectsQuery } from 'data/projects/projects-query' import { checkPermissions, useStore } from 'hooks' import { post } from 'lib/common/fetch' import { API_URL, PROJECT_STATUS } from 'lib/constants' -import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' +import { Button, IconPause } from 'ui' interface Props { projectId: number @@ -16,7 +19,9 @@ interface Props { } const PauseProjectButton: FC = observer(({ projectRef, projectId }) => { - const { ui, app } = useStore() + const queryClient = useQueryClient() + const { ui } = useStore() + const { project } = useProjectContext() const router = useRouter() const [loading, setLoading] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false) @@ -24,7 +29,7 @@ const PauseProjectButton: FC = observer(({ projectRef, projectId }) => { const openModal = () => setIsModalOpen(true) const closeModal = () => setIsModalOpen(false) - const isPaused = ui?.selectedProject?.status === PROJECT_STATUS.INACTIVE + const isPaused = project?.status === PROJECT_STATUS.INACTIVE const canPauseProject = checkPermissions( PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.pause' @@ -49,8 +54,7 @@ const PauseProjectButton: FC = observer(({ projectRef, projectId }) => { }) setLoading(false) } else { - app.onProjectPaused(projectId) - app.onProjectStatusUpdated(projectId, PROJECT_STATUS.GOING_DOWN) + await invalidateProjectsQuery(queryClient) ui.setNotification({ category: 'success', message: 'Pausing project' }) router.push(`/project/${projectRef}`) diff --git a/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx b/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx index 8099afc7b63..36bbd1557e6 100644 --- a/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx +++ b/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx @@ -1,22 +1,25 @@ -import { FC, useState } from 'react' -import { useRouter } from 'next/router' -import { observer } from 'mobx-react-lite' import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useQueryClient } from '@tanstack/react-query' +import { observer } from 'mobx-react-lite' +import { useRouter } from 'next/router' +import { FC, useState } from 'react' import { Button, Dropdown, IconChevronDown } from 'ui' -import { Project } from 'types' -import { useStore, checkPermissions } from 'hooks' +import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' +import { invalidateProjectsQuery } from 'data/projects/projects-query' +import { checkPermissions, useStore } from 'hooks' import { post } from 'lib/common/fetch' import { API_URL } from 'lib/constants' -import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' +import { Project } from 'types' interface Props { project: Project } const RestartServerButton: FC = ({ project }) => { - const { ui, app } = useStore() + const queryClient = useQueryClient() + const { ui } = useStore() const router = useRouter() const [loading, setLoading] = useState(false) const [serviceToRestart, setServiceToRestart] = useState<'project' | 'database'>() @@ -69,7 +72,7 @@ const RestartServerButton: FC = ({ project }) => { } const onRestartSuccess = () => { - app.onProjectPostgrestStatusUpdated(projectId, 'OFFLINE') + invalidateProjectsQuery(queryClient) ui.setNotification({ category: 'success', message: 'Restarting server' }) router.push(`/project/${projectRef}`) setLoading(false) diff --git a/studio/components/interfaces/Support/SupportForm.tsx b/studio/components/interfaces/Support/SupportForm.tsx index 00d71999044..f3cb3f15ed1 100644 --- a/studio/components/interfaces/Support/SupportForm.tsx +++ b/studio/components/interfaces/Support/SupportForm.tsx @@ -1,7 +1,6 @@ import { CLIENT_LIBRARIES } from 'common/constants' import { observer } from 'mobx-react-lite' import Link from 'next/link' -import { useRouter } from 'next/router' import { ChangeEvent, FC, useEffect, useRef, useState } from 'react' import { Button, @@ -17,6 +16,7 @@ import { Listbox, } from 'ui' +import { useParams } from 'common' import Divider from 'components/ui/Divider' import InformationBox from 'components/ui/InformationBox' import Connecting from 'components/ui/Loading' @@ -24,10 +24,11 @@ import MultiSelect from 'components/ui/MultiSelect' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProfileQuery } from 'data/profile/profile-query' import { useProjectsQuery } from 'data/projects/projects-query' +import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' import { useFlag, useStore } from 'hooks' import useLatest from 'hooks/misc/useLatest' import { get, post } from 'lib/common/fetch' -import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants' +import { API_URL } from 'lib/constants' import { detectBrowser } from 'lib/helpers' import { Project } from 'types' import DisabledStateForFreeTier from './DisabledStateForFreeTier' @@ -41,9 +42,8 @@ interface Props { } const SupportForm: FC = ({ setSentCategory }) => { - const { ui, app } = useStore() - const router = useRouter() - const { ref, subject, category, message } = router.query + const { ui } = useStore() + const { ref, subject, category, message } = useParams() const uploadButtonRef = useRef() const enableFreeSupport = useFlag('enableFreeSupport') @@ -56,20 +56,15 @@ const SupportForm: FC = ({ setSentCategory }) => { const organizationsRef = useLatest(organizations) const { data: allProjects, isSuccess: isProjectsSuccess } = useProjectsQuery() + const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionV2Query({ + projectRef: ref, + }) const isInitialized = isOrganizationsSuccess && isProjectsSuccess const projectDefaults: Partial[] = [{ ref: 'no-project', name: 'No specific project' }] const projects = [...(allProjects ?? []), ...projectDefaults] - const planNames = { - [PRICING_TIER_PRODUCT_IDS.FREE]: 'Free', - [PRICING_TIER_PRODUCT_IDS.PRO]: 'Pro', - [PRICING_TIER_PRODUCT_IDS.PAYG]: 'Pro', - [PRICING_TIER_PRODUCT_IDS.TEAM]: 'Team', - [PRICING_TIER_PRODUCT_IDS.ENTERPRISE]: 'Enterprise', - } - useEffect(() => { if (!uploadedFiles) return const objectUrls = uploadedFiles.map((file) => URL.createObjectURL(file)) @@ -213,9 +208,7 @@ const SupportForm: FC = ({ setSentCategory }) => { ) const selectedProject = projects.find((project) => project.ref === values.projectRef) - const isFreeProject = - (selectedProject?.subscription_tier ?? PRICING_TIER_PRODUCT_IDS.FREE) === - PRICING_TIER_PRODUCT_IDS.FREE + const isFreeProject = (subscription?.plan.id ?? 'free') === 'free' const isDisabled = !enableFreeSupport && isFreeProject && @@ -232,9 +225,6 @@ const SupportForm: FC = ({ setSentCategory }) => { } resetForm({ values: updatedValues, initialValues: updatedValues }) } else if (selectedProject) { - if (!selectedProject.subscription_tier) { - app.projects.fetchSubscriptionTier(selectedProject as Project) - } const organization = organizationsRef.current?.find( (org) => org.id === selectedProject.organization_id ) @@ -305,14 +295,12 @@ const SupportForm: FC = ({ setSentCategory }) => { })} - {selectedProject?.subscription_tier ? ( + {subscription ? (

This project is on the{' '} - - {planNames[selectedProject?.subscription_tier]} plan - + {subscription.plan.name} plan

- ) : selectedProject?.ref !== 'no-project' ? ( + ) : isLoadingSubscription ? (

Checking project's plan

@@ -344,7 +332,7 @@ const SupportForm: FC = ({ setSentCategory }) => {
)} - {selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.FREE && ( + {subscription?.plan.id === 'free' && (
} diff --git a/studio/components/layouts/ProjectLayout/BuildingState.tsx b/studio/components/layouts/ProjectLayout/BuildingState.tsx index 11ec2af1c9e..0aae7f41a4e 100644 --- a/studio/components/layouts/ProjectLayout/BuildingState.tsx +++ b/studio/components/layouts/ProjectLayout/BuildingState.tsx @@ -1,25 +1,26 @@ +import { useQueryClient } from '@tanstack/react-query' +import ClientLibrary from 'components/interfaces/Home/ClientLibrary' +import ExampleProject from 'components/interfaces/Home/ExampleProject' +import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants' import Link from 'next/link' import { FC, useEffect, useRef } from 'react' -import { observer } from 'mobx-react-lite' -import { Badge, IconArrowRight, IconLoader, Button } from 'ui' -import ExampleProject from 'components/interfaces/Home/ExampleProject' -import ClientLibrary from 'components/interfaces/Home/ClientLibrary' -import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants' +import { Badge, Button, IconArrowRight, IconLoader } from 'ui' -import { API_URL, PROJECT_STATUS } from 'lib/constants' -import { useStore } from 'hooks' -import { getWithTimeout } from 'lib/common/fetch' -import { Project } from 'types' import { DisplayApiSettings, DisplayConfigSettings } from 'components/ui/ProjectSettings' +import { invalidateProjectsQuery } from 'data/projects/projects-query' +import { getWithTimeout } from 'lib/common/fetch' +import { API_URL, PROJECT_STATUS } from 'lib/constants' +import { Project } from 'types' interface Props { project: Project } const BuildingState: FC = ({ project }) => { - const { ui, app } = useStore() + const queryClient = useQueryClient() const checkServerInterval = useRef() + // TODO: move to react-query async function checkServer() { if (!project) return @@ -30,9 +31,8 @@ const BuildingState: FC = ({ project }) => { const { status } = projectStatus if (status === PROJECT_STATUS.ACTIVE_HEALTHY) { clearInterval(checkServerInterval.current) - // re-fetch project detail. - // This will also trigger UI state change to show project building completed - await app.projects.fetchDetail(project.ref) + + await invalidateProjectsQuery(queryClient) } } } @@ -156,7 +156,7 @@ const BuildingState: FC = ({ project }) => {
) } -export default observer(BuildingState) +export default BuildingState const ChecklistItem = ({ description }: any) => { return ( diff --git a/studio/components/layouts/ProjectLayout/ConnectingState.tsx b/studio/components/layouts/ProjectLayout/ConnectingState.tsx index 4378343983b..4c34bdd2650 100644 --- a/studio/components/layouts/ProjectLayout/ConnectingState.tsx +++ b/studio/components/layouts/ProjectLayout/ConnectingState.tsx @@ -1,19 +1,19 @@ +import { useQueryClient } from '@tanstack/react-query' import Link from 'next/link' import { FC, useEffect, useRef, useState } from 'react' -import { observer } from 'mobx-react-lite' import { Badge, Button, IconLoader, IconMonitor, IconServer, Modal } from 'ui' -import { Project } from 'types' -import { useStore } from 'hooks' import ShimmerLine from 'components/ui/ShimmerLine' +import { invalidateProjectsQuery } from 'data/projects/projects-query' import pingPostgrest from 'lib/pingPostgrest' +import { Project } from 'types' interface Props { project: Project } const ConnectingState: FC = ({ project }) => { - const { app } = useStore() + const queryClient = useQueryClient() const checkProjectConnectionIntervalRef = useRef() const [showHelperButton, setShowHelperButton] = useState(false) @@ -40,7 +40,7 @@ const ConnectingState: FC = ({ project }) => { const result = await pingPostgrest(project.ref, { kpsVersion: project.kpsVersion }) if (result) { clearInterval(checkProjectConnectionIntervalRef.current) - app.onProjectPostgrestStatusUpdated(project.id, 'ONLINE') + await invalidateProjectsQuery(queryClient) } } @@ -144,4 +144,4 @@ const ConnectingState: FC = ({ project }) => { ) } -export default observer(ConnectingState) +export default ConnectingState diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx index 129a2568e6c..e487c7d0abb 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx @@ -1,27 +1,31 @@ -import dayjs from 'dayjs' -import { FC, Fragment, useState } from 'react' -import { useRouter } from 'next/router' -import { Alert, Button, IconBell, Popover, IconArrowRight } from 'ui' import * as Tooltip from '@radix-ui/react-tooltip' import { + ActionType, Notification, NotificationStatus, - ActionType, } from '@supabase/shared-types/out/notifications' +import { useQueryClient } from '@tanstack/react-query' +import dayjs from 'dayjs' +import { useRouter } from 'next/router' +import { FC, Fragment, useState } from 'react' -import { Project } from 'types' +import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' +import { useNotificationsQuery } from 'data/notifications/notifications-query' +import { getProjectDetail } from 'data/projects/project-detail-query' +import { invalidateProjectsQuery } from 'data/projects/projects-query' import { useStore } from 'hooks' import { delete_, patch, post } from 'lib/common/fetch' import { API_URL } from 'lib/constants' +import { Project } from 'types' +import { Alert, Button, IconArrowRight, IconBell, Popover } from 'ui' import NotificationRow from './NotificationRow' -import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' -import { useNotificationsQuery } from 'data/notifications/notifications-query' interface Props {} const NotificationsPopover: FC = () => { + const queryClient = useQueryClient() const router = useRouter() - const { app, meta, ui } = useStore() + const { meta, ui } = useStore() const { data: notifications, refetch } = useNotificationsQuery() const [projectToRestart, setProjectToRestart] = useState() @@ -82,7 +86,7 @@ const NotificationsPopover: FC = () => { error, }) } else { - app.onProjectPostgrestStatusUpdated(projectId, 'OFFLINE') + await invalidateProjectsQuery(queryClient) ui.setNotification({ category: 'success', message: `Restarting services` }) router.push(`/project/${ref}`) } @@ -102,9 +106,11 @@ const NotificationsPopover: FC = () => { if (!projectToApplyMigration) return const res = await post(`${API_URL}/database/${projectToApplyMigration.ref}/owner-reassign`, {}) if (!res.error) { - await app.projects.fetchDetail(projectToApplyMigration.ref, (project) => + const project = await getProjectDetail({ ref: projectToApplyMigration.ref }) + if (project) { meta.setProjectDetails(project) - ) + } + ui.setNotification({ category: 'success', message: `Successfully applied migration for project "${projectToApplyMigration.name}"`, @@ -126,9 +132,11 @@ const NotificationsPopover: FC = () => { {} ) if (!res.error) { - await app.projects.fetchDetail(projectToRollbackMigration.ref, (project) => + const project = await getProjectDetail({ ref: projectToRollbackMigration.ref }) + if (project) { meta.setProjectDetails(project) - ) + } + ui.setNotification({ category: 'success', message: `Successfully rolled back migration for project "${projectToRollbackMigration.name}"`, @@ -150,9 +158,11 @@ const NotificationsPopover: FC = () => { {} ) if (!res.error) { - await app.projects.fetchDetail(projectToFinalizeMigration.ref, (project) => + const project = await getProjectDetail({ ref: projectToFinalizeMigration.ref }) + if (project) { meta.setProjectDetails(project) - ) + } + ui.setNotification({ category: 'success', message: `Successfully finalized migration for project "${projectToFinalizeMigration.name}"`, diff --git a/studio/components/layouts/ProjectLayout/RestoringState.tsx b/studio/components/layouts/ProjectLayout/RestoringState.tsx index 8c40d1c716f..cf70e2a914e 100644 --- a/studio/components/layouts/ProjectLayout/RestoringState.tsx +++ b/studio/components/layouts/ProjectLayout/RestoringState.tsx @@ -1,6 +1,7 @@ import Link from 'next/link' import { FC, useEffect, useRef, useState } from 'react' +import { getProjectDetail } from 'data/projects/project-detail-query' import { useStore } from 'hooks' import { getWithTimeout } from 'lib/common/fetch' import { API_URL, PROJECT_STATUS } from 'lib/constants' @@ -10,7 +11,7 @@ import { useProjectContext } from './ProjectContext' interface Props {} const RestoringState: FC = ({}) => { - const { app, ui, meta } = useStore() + const { meta } = useStore() const { project } = useProjectContext() const checkServerInterval = useRef() @@ -42,8 +43,13 @@ const RestoringState: FC = ({}) => { } const onConfirm = async () => { + if (!project) return + setLoading(true) - await app.projects.fetchDetail(project?.ref ?? '', (project) => meta.setProjectDetails(project)) + const projectDetail = await getProjectDetail({ ref: project?.ref }) + if (projectDetail) { + meta.setProjectDetails(projectDetail) + } } return ( diff --git a/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx b/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx index 9436938f079..2f6b15649e5 100644 --- a/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx +++ b/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx @@ -1,30 +1,31 @@ +import * as Tooltip from '@radix-ui/react-tooltip' +import { DatabaseUpgradeStatus } from '@supabase/shared-types/out/events' import dayjs from 'dayjs' import Link from 'next/link' import { useState } from 'react' -import * as Tooltip from '@radix-ui/react-tooltip' import { Button, - IconCircle, - IconCheckCircle, - IconSettings, IconAlertCircle, - IconLoader, IconCheck, + IconCheckCircle, + IconCircle, + IconLoader, IconMaximize2, IconMinimize2, + IconSettings, } from 'ui' -import { DatabaseUpgradeStatus } from '@supabase/shared-types/out/events' -import { useStore } from 'hooks' import { useParams } from 'common/hooks' -import { IS_PLATFORM } from 'lib/constants' -import { DATABASE_UPGRADE_MESSAGES } from './UpgradingState.constants' import { useProjectUpgradingStatusQuery } from 'data/config/project-upgrade-status-query' +import { getProjectDetail } from 'data/projects/project-detail-query' +import { useStore } from 'hooks' +import { IS_PLATFORM } from 'lib/constants' import { useProjectContext } from '../ProjectContext' +import { DATABASE_UPGRADE_MESSAGES } from './UpgradingState.constants' const UpgradingState = () => { const { ref } = useParams() - const { app, meta } = useStore() + const { meta } = useStore() const { project } = useProjectContext() const [loading, setLoading] = useState(false) const [isExpanded, setIsExpanded] = useState(false) @@ -52,7 +53,11 @@ const UpgradingState = () => { const refetchProjectDetails = async () => { setLoading(true) - await app.projects.fetchDetail(project?.ref ?? '', (project) => meta.setProjectDetails(project)) + + const projectDetail = await getProjectDetail({ ref }) + if (projectDetail) { + meta.setProjectDetails(projectDetail) + } } const subject = 'Upgrade%20failed%20for%20project' diff --git a/studio/hooks/misc/useStore.tsx b/studio/hooks/misc/useStore.tsx index 2248cda5eee..025301498b5 100644 --- a/studio/hooks/misc/useStore.tsx +++ b/studio/hooks/misc/useStore.tsx @@ -35,8 +35,6 @@ export const StoreProvider: FC = ({ children, rootStore }) => { }, [isDarkMode, monaco]) useEffect(() => { - ui.load() - autorun(() => { if (ui.notification) { const { id, category, error, message, description, progress, duration } = ui.notification diff --git a/studio/hooks/misc/withAuth.tsx b/studio/hooks/misc/withAuth.tsx index 3daa4716f66..3c0216cc2b8 100644 --- a/studio/hooks/misc/withAuth.tsx +++ b/studio/hooks/misc/withAuth.tsx @@ -4,11 +4,11 @@ import { ComponentType, useEffect } from 'react' import { useParams } from 'common/hooks' import { usePermissionsQuery } from 'data/permissions/permissions-query' -import { useStore } from 'hooks' +import { useSelectedProject, useStore } from 'hooks' import { useAuth } from 'lib/auth' import { IS_PLATFORM } from 'lib/constants' -import { getReturnToPath, STORAGE_KEY } from 'lib/gotrue' -import { isNextPageWithLayout, NextPageWithLayout } from 'types' +import { STORAGE_KEY, getReturnToPath } from 'lib/gotrue' +import { NextPageWithLayout, isNextPageWithLayout } from 'types' import Error500 from '../../pages/500' const PLATFORM_ONLY_PAGES = [ @@ -70,14 +70,12 @@ export function withAuth( } }, [isRedirecting, redirectTo]) + const selectedProject = useSelectedProject() useEffect(() => { - if (router.isReady) { - if (ref) { - rootStore.setProjectRef(ref) - } - rootStore.setOrganizationSlug(slug) + if (selectedProject) { + rootStore.setProject(selectedProject) } - }, [isLoading, router.isReady, ref, slug]) + }, [selectedProject]) if (!isLoading && !isRedirecting && !isLoggedIn) { return diff --git a/studio/pages/join/index.tsx b/studio/pages/join/index.tsx index a87cffe2e10..cbfc23909ae 100644 --- a/studio/pages/join/index.tsx +++ b/studio/pages/join/index.tsx @@ -1,13 +1,16 @@ +import { useQueryClient } from '@tanstack/react-query' import Link from 'next/link' -import { useState, useEffect } from 'react' import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' import { Button, IconCheckSquare, Loading } from 'ui' +import { useParams } from 'common' +import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query' +import { useProfileQuery } from 'data/profile/profile-query' import { useStore } from 'hooks' import { useSignOut } from 'lib/auth' +import { delete_, get, post } from 'lib/common/fetch' import { API_URL } from 'lib/constants' -import { get, post, delete_ } from 'lib/common/fetch' -import { useProfileQuery } from 'data/profile/profile-query' interface ITokenInfo { organization_name?: string | undefined @@ -21,9 +24,10 @@ interface ITokenInfo { type TokenInfo = ITokenInfo | undefined const JoinOrganizationPage = () => { + const queryClient = useQueryClient() const router = useRouter() - const { slug, token, name } = router.query - const { ui, app } = useStore() + const { slug, token, name } = useParams() + const { ui } = useStore() const { data: profile } = useProfileQuery() const signOut = useSignOut() @@ -38,8 +42,6 @@ const JoinOrganizationPage = () => { useEffect(() => { const fetchTokenInfo = async () => { - await ui.load() - const response = await get(`${API_URL}/organizations/${slug}/members/join?token=${token}`) if (response.error) { @@ -74,7 +76,7 @@ const JoinOrganizationPage = () => { setIsSubmitting(false) } else { setIsSubmitting(false) - app.onOrgAdded(response) + await invalidateOrganizationsQuery(queryClient) router.push('/') } } diff --git a/studio/stores/RootStore.ts b/studio/stores/RootStore.ts index 3b42662f36d..ed7bd2f8eb9 100644 --- a/studio/stores/RootStore.ts +++ b/studio/stores/RootStore.ts @@ -29,8 +29,8 @@ export interface IRootStore { selectedProjectRef?: string - setProjectRef: (value: string) => void - setOrganizationSlug: (value?: string) => void + setProject: (project: Project) => void + // setOrganizationSlug: (value?: string) => void } export class RootStore implements IRootStore { app: IAppStore @@ -61,40 +61,38 @@ export class RootStore implements IRootStore { * * This method will also trigger project detail loading when it's not available */ - setProjectRef(value: string) { - if (this.selectedProjectRef === value) return - this.selectedProjectRef = value + setProject(project: Project) { + if (this.selectedProjectRef === project.ref) return + this.selectedProjectRef = project.ref // reset ui projectRef in case of switching projects // this will show the loading screen instead of showing the previous project this.ui.setProjectRef(undefined) - const setProjectRefs = (project: Project) => { - this.meta.setProjectDetails(project) - this.functions.setProjectRef(project.ref) - this.authConfig.setProjectRef(project.ref) - this.content.setProjectRef(project.ref) - this.backups.setProjectRef(project.ref) - // ui set must come last - this.ui.setProjectRef(project.ref) - } + this.meta.setProjectDetails(project) + this.functions.setProjectRef(project.ref) + this.authConfig.setProjectRef(project.ref) + this.content.setProjectRef(project.ref) + this.backups.setProjectRef(project.ref) + // ui set must come last + this.ui.setProjectRef(project.ref) // fetch project detail when // - project not found yet. projectStore is loading // - connectionString is not available. projectStore loaded - const found = this.app.projects.find((x: Project) => x.ref === value) - if (found?.connectionString === undefined) { - this.app.projects.fetchDetail(value, (project) => { - setProjectRefs(project) - }) - } else { - setProjectRefs(found) - } + // const found = this.app.projects.find((x: Project) => x.ref === value) + // if (found?.connectionString === undefined) { + // this.app.projects.fetchDetail(value, (project) => { + // setProjectRefs(project) + // }) + // } else { + // setProjectRefs(found) + // } } - setOrganizationSlug(value?: string) { - if (this.ui.selectedOrganization?.slug != value) { - this.ui.setOrganizationSlug(value) - } - } + // setOrganizationSlug(value?: string) { + // if (this.ui.selectedOrganization?.slug != value) { + // this.ui.setOrganizationSlug(value) + // } + // } } diff --git a/studio/stores/UiStore.ts b/studio/stores/UiStore.ts index 0e7b35339f9..da147bda746 100644 --- a/studio/stores/UiStore.ts +++ b/studio/stores/UiStore.ts @@ -6,12 +6,12 @@ import { IRootStore } from './RootStore' export interface IUiStore { language: 'en-US' selectedProjectRef?: string - selectedProject?: Project - selectedProjectBaseInfo?: ProjectBase - selectedOrganization?: Organization + // selectedProject?: Project + // selectedProjectBaseInfo?: ProjectBase + // selectedOrganization?: Organization notification?: Notification permissions?: Permission[] - load: () => void + // load: () => void setProjectRef: (ref?: string) => void setOrganizationSlug: (slug?: string) => void setNotification: (notification: Notification) => string @@ -40,49 +40,49 @@ export default class UiStore implements IUiStore { * * @returns Project or undefined */ - get selectedProject() { - if (this.selectedProjectRef) { - const found = this.rootStore.app.projects.find( - (x: Project) => x.ref === this.selectedProjectRef - ) - // Self-hosted project details has connectionString set to empty string - return found?.connectionString !== undefined ? found : undefined - } - return undefined - } + // get selectedProject() { + // if (this.selectedProjectRef) { + // const found = this.rootStore.app.projects.find( + // (x: Project) => x.ref === this.selectedProjectRef + // ) + // // Self-hosted project details has connectionString set to empty string + // return found?.connectionString !== undefined ? found : undefined + // } + // return undefined + // } /** * Get selected project base info. * * @return ProjectBase or undefined */ - get selectedProjectBaseInfo(): ProjectBase | undefined { - if (this.selectedProjectRef) { - return this.rootStore.app.projects.find((x: Project) => x.ref == this.selectedProjectRef) - } - return undefined - } + // get selectedProjectBaseInfo(): ProjectBase | undefined { + // if (this.selectedProjectRef) { + // return this.rootStore.app.projects.find((x: Project) => x.ref == this.selectedProjectRef) + // } + // return undefined + // } - get selectedOrganization() { - if (this.selectedOrganizationSlug) { - const found = this.rootStore.app.organizations.find( - (x: Organization) => x.slug == this.selectedOrganizationSlug - ) - return found - } - if (this.selectedProjectRef) { - const organizationId = this.selectedProject?.organization_id - const found = this.rootStore.app.organizations.find( - (x: Organization) => x.id == organizationId - ) - return found - } - return undefined - } + // get selectedOrganization() { + // if (this.selectedOrganizationSlug) { + // const found = this.rootStore.app.organizations.find( + // (x: Organization) => x.slug == this.selectedOrganizationSlug + // ) + // return found + // } + // if (this.selectedProjectRef) { + // const organizationId = this.selectedProject?.organization_id + // const found = this.rootStore.app.organizations.find( + // (x: Organization) => x.id == organizationId + // ) + // return found + // } + // return undefined + // } - load() { - if (typeof window === 'undefined') return - } + // load() { + // if (typeof window === 'undefined') return + // } setProjectRef(ref?: string) { this.selectedProjectRef = ref diff --git a/studio/stores/app/AppStore.ts b/studio/stores/app/AppStore.ts index 6b436e9ddb9..4c0a84b878b 100644 --- a/studio/stores/app/AppStore.ts +++ b/studio/stores/app/AppStore.ts @@ -1,32 +1,12 @@ -import { cloneDeep } from 'lodash' -import { values } from 'mobx' -import { Organization, Project } from 'types' - -import { API_URL, PROJECT_STATUS } from 'lib/constants' -import { getWithTimeout } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' import { IRootStore } from '../RootStore' import DatabaseStore, { IDatabaseStore } from './DatabaseStore' -import OrganizationStore from './OrganizationStore' -import ProjectStore, { IProjectStore } from './ProjectStore' export interface IAppStore { - projects: IProjectStore - organizations: OrganizationStore database: IDatabaseStore - onProjectCreated: (project: Project) => void - onProjectUpdated: (project: Project) => void - onProjectDeleted: (project: Project) => void - onProjectPaused: (projectId: number) => void - onProjectStatusUpdated: (projectId: number, value: string) => void - onProjectPostgrestStatusUpdated: (projectId: number, value: 'OFFLINE' | 'ONLINE') => void - onOrgAdded: (org: any) => void - onOrgUpdated: (org: any) => void - onOrgDeleted: (org: any) => void } export default class AppStore implements IAppStore { rootStore: IRootStore - projects: ProjectStore - organizations: OrganizationStore database: DatabaseStore baseUrl: string @@ -36,114 +16,6 @@ export default class AppStore implements IAppStore { this.baseUrl = API_URL || '/api' const headers: any = {} - - this.projects = new ProjectStore(rootStore, `${this.baseUrl}/projects`, headers) - this.organizations = new OrganizationStore(rootStore, `${this.baseUrl}/organizations`, headers) this.database = new DatabaseStore(rootStore, `${this.baseUrl}/database`, headers) } - - onProjectCreated(project: any) { - if (project && project.id) { - const temp: Project = { - id: project.id, - ref: project.ref, - name: project.name, - status: project.status, - organization_id: project.organization_id, - cloud_provider: project.cloud_provider, - region: project.region, - inserted_at: project.inserted_at, - subscription_id: project.subscription_id, - } - this.projects.data[project.id] = temp - } - } - - onProjectUpdated(project: any) { - if (project && project.id) { - const clone = cloneDeep(this.projects.data[project.id]) - // only update available param - if (project.name) clone.name = project.name - if (project.status) clone.status = project.status - this.projects.data[project.id] = clone - } - } - - onProjectDeleted(project: any) { - if (project && project.id) { - // cleanup project saved queries - localStorage.removeItem(`supabase-queries-state-${project.ref}`) - localStorage.removeItem(`supabase_${project.ref}`) - delete this.projects.data[project.id] - } - } - - // At global store level so that it can continue checking while the user - // is doing something else outside of the project page - async onProjectPaused(projectId: number) { - const checkProjectInactive = async () => { - const projectRef = this.projects.data[projectId]?.ref - const projectStatus = await getWithTimeout(`${API_URL}/projects/${projectRef}/status`, { - timeout: 2000, - }) - if (projectStatus.status === PROJECT_STATUS.INACTIVE) { - this.onProjectStatusUpdated(projectId, PROJECT_STATUS.INACTIVE) - } else { - setTimeout(() => checkProjectInactive(), 5000) - } - } - - setTimeout(() => checkProjectInactive(), 5000) - } - - onProjectStatusUpdated(projectId: number, value: string) { - const clone = cloneDeep(this.projects.data[projectId]) - clone.status = value - this.projects.data[projectId] = clone - } - - onProjectPostgrestStatusUpdated(projectId: number, value: 'OFFLINE' | 'ONLINE') { - const clone = cloneDeep(this.projects.data[projectId]) - clone.postgrestStatus = value - this.projects.data[projectId] = clone - } - - onOrgUpdated(updatedOrg: Organization) { - if (updatedOrg && updatedOrg.id) { - const originalOrg = this.organizations.data[updatedOrg.id] - this.organizations.data[updatedOrg.id] = { - ...originalOrg, - ...updatedOrg, - } - } - } - - onOrgAdded(org: Organization) { - if (org && org.id) { - this.organizations.data[org.id] = org - } - } - - onOrgDeleted(org: Organization) { - if (org && org.id) { - const projects = values(this.projects.data) - // cleanup projects saved queries - const removedprojects = projects.filter( - (project: Project) => project.organization_id === org.id - ) - removedprojects.forEach((project: Project) => { - localStorage.removeItem(`supabase-queries-state-${project.ref}`) - localStorage.removeItem(`supabase_${project.ref}`) - }) - // remove projects belong to removed org - const updatedProjects = projects.filter( - (project: Project) => project.organization_id != org.id - ) - this.projects.data = updatedProjects.reduce((map: any, x: any) => { - map[x.id] = { ...x } - return map - }, {}) - delete (this.organizations.data as any)[org.id] - } - } } diff --git a/studio/stores/app/OrganizationStore.ts b/studio/stores/app/OrganizationStore.ts deleted file mode 100644 index daa048cc96d..00000000000 --- a/studio/stores/app/OrganizationStore.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Organization } from 'types' -import { IRootStore } from '../RootStore' -import PostgresMetaInterface from '../common/PostgresMetaInterface' - -export default class OrganizationStore extends PostgresMetaInterface { - constructor( - rootStore: IRootStore, - dataUrl: string, - headers?: { - [prop: string]: any - }, - options?: { identifier: string } - ) { - super(rootStore, dataUrl, headers, options) - } -} diff --git a/studio/stores/app/PermissionsStore.ts b/studio/stores/app/PermissionsStore.ts deleted file mode 100644 index a14f98cc1d0..00000000000 --- a/studio/stores/app/PermissionsStore.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { keyBy } from 'lodash' -import { Permission } from 'types' -import { IRootStore } from '../RootStore' -import PostgresMetaInterface from '../common/PostgresMetaInterface' - -export default class PermissionsStore extends PostgresMetaInterface { - constructor( - rootStore: IRootStore, - dataUrl: string, - headers?: { - [prop: string]: any - }, - options?: { identifier: string } - ) { - super(rootStore, dataUrl, headers, options) - } - - // [Joshen] Method override here as permissions are not identified by an id from the API - setDataArray(value: any[]) { - const formattedValues = value.map((permission, idx: number) => { - return { ...permission, id: idx + 1 } - }) - const formattedValue = keyBy(formattedValues, this.identifier) - Object.keys(this.data).forEach((projectId) => { - if (formattedValue[projectId] !== undefined) { - formattedValue[projectId] = this.data[projectId] - } - }) - this.data = formattedValue - } - - // [Joshen] Method override here as permissions do not have `name` property - list() { - return Object.values(this.data) - } -} diff --git a/studio/stores/app/ProjectStore.ts b/studio/stores/app/ProjectStore.ts deleted file mode 100644 index d43814c29a6..00000000000 --- a/studio/stores/app/ProjectStore.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { cloneDeep, keyBy } from 'lodash' -import { Project } from 'types' -import { IRootStore } from '../RootStore' -import { constructHeaders } from 'lib/api/apiHelpers' -import { get } from 'lib/common/fetch' -import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' -import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/PostgresMetaInterface' - -import pingPostgrest from 'lib/pingPostgrest' - -export interface IProjectStore extends IPostgresMetaInterface { - fetchDetail: (projectRef: string, callback?: (project: Project) => void) => Promise - fetchSubscriptionTier: (project: Project) => Promise -} - -export default class ProjectStore extends PostgresMetaInterface { - constructor( - rootStore: IRootStore, - dataUrl: string, - headers?: { - [prop: string]: any - }, - options?: { identifier: string } - ) { - super(rootStore, dataUrl, headers, options) - } - - // [Joshen] Method override here due to race condition between projects.load() and fetchDetail() - // There's a race condition that can happen here when these 2 methods are called, such that if - // fetchDetail() completes before projects.load() completes, the response from project.loads() - // then overrides what fetchDetail() has already populated into the store - causing parameters like - // connectionString to become undefined and hence causing the dashboard to stay in a Connecting state. - // You can reproduce this by giving a setTimeout to project.loads under withAuth.tsx - - // This thus ensures that the response from project.loads() do not override projects which are already - // loaded in the store from a successful fetchDetail() - This should be fine as project.loads() is only - // called once in this whole app under withAuth.tsx - setDataArray(value: any[]) { - const formattedValue = keyBy(value, this.identifier) - Object.keys(this.data).forEach((projectId) => { - if (formattedValue[projectId] !== undefined) { - formattedValue[projectId] = this.data[projectId] - } - }) - this.data = formattedValue - } - - async fetchDetail(projectRef: string, callback?: (project: Project) => void) { - const url = `${this.url}/${projectRef}` - const headers = constructHeaders(this.headers) - const response = await get(url, { headers }) - - if (!response.error) { - const project = response as Project - // to improve UX, we wait for PingPostgrest result before continue - project.postgrestStatus = await this.pingPostgrest(project) - // update project detail by key id - this.data[project.id] = project - - callback?.(project) - - // lazy fetches - if (IS_PLATFORM) { - this.fetchSubscriptionTier(project) - } - } - } - - async pingPostgrest(project: Project): Promise<'ONLINE' | 'OFFLINE' | undefined> { - if (project.status === PROJECT_STATUS.ACTIVE_HEALTHY && project.restUrl) { - const success = await pingPostgrest(project.ref, { kpsVersion: project.kpsVersion }) - return success ? 'ONLINE' : 'OFFLINE' - } - return undefined - } - - async fetchSubscriptionTier(project: Project) { - const { id: projectId, ref: projectRef, status } = project - if (status !== PROJECT_STATUS.REMOVED) { - const url = `${this.url}/${projectRef}/subscription` - const headers = constructHeaders(this.headers) - const response = await get(url, { headers }) - if (!response.error) { - const subscriptionInfo = response as { - tier: { - supabase_prod_id: string - } - } - // update subscription_tier key - const clone = cloneDeep(this.data[projectId]) - clone.subscription_tier = subscriptionInfo.tier.supabase_prod_id - this.data[projectId] = clone - } - } - } -}