delete org and project stores

This commit is contained in:
Alaister Young committed 2023-06-20 22:31:18 +10:00
1 parent a99faa1580
commit 159f982374
19 files changed
+206 -462

No files matched your search

@@ -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)
@@ -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<any> = ({ disabled = false }) => {
const { ui, app, meta } = useStore()
@@ -65,7 +66,11 @@ const ResetDbPassword: FC<any> = ({ 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 {
@@ -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<Props> = 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<Props> = 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<Props> = 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}`)
@@ -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<Props> = ({ 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<Props> = ({ project }) => {
}
const onRestartSuccess = () => {
app.onProjectPostgrestStatusUpdated(projectId, 'OFFLINE')
invalidateProjectsQuery(queryClient)
ui.setNotification({ category: 'success', message: 'Restarting server' })
router.push(`/project/${projectRef}`)
setLoading(false)
@@ -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<Props> = ({ 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<Props> = ({ 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<Project>[] = [{ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ setSentCategory }) => {
})}
</Listbox>
</div>
{selectedProject?.subscription_tier ? (
{subscription ? (
<p className="text-sm text-scale-1000 mt-2">
This project is on the{' '}
<span className="text-scale-1100">
{planNames[selectedProject?.subscription_tier]} plan
</span>
<span className="text-scale-1100">{subscription.plan.name} plan</span>
</p>
) : selectedProject?.ref !== 'no-project' ? (
) : isLoadingSubscription ? (
<div className="flex items-center space-x-2 mt-2">
<IconLoader size={14} className="animate-spin" />
<p className="text-sm text-scale-1000">Checking project's plan</p>
@@ -344,7 +332,7 @@ const SupportForm: FC<Props> = ({ setSentCategory }) => {
</div>
)}
{selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.FREE && (
{subscription?.plan.id === 'free' && (
<div className="px-6">
<InformationBox
icon={<IconAlertCircle strokeWidth={2} />}
@@ -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<Props> = ({ project }) => {
const { ui, app } = useStore()
const queryClient = useQueryClient()
const checkServerInterval = useRef<number>()
// TODO: move to react-query
async function checkServer() {
if (!project) return
@@ -30,9 +31,8 @@ const BuildingState: FC<Props> = ({ 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<Props> = ({ project }) => {
</div>
)
}
export default observer(BuildingState)
export default BuildingState
const ChecklistItem = ({ description }: any) => {
return (
@@ -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<Props> = ({ project }) => {
const { app } = useStore()
const queryClient = useQueryClient()
const checkProjectConnectionIntervalRef = useRef<number>()
const [showHelperButton, setShowHelperButton] = useState(false)
@@ -40,7 +40,7 @@ const ConnectingState: FC<Props> = ({ 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<Props> = ({ project }) => {
)
}
export default observer(ConnectingState)
export default ConnectingState
@@ -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<Props> = () => {
const queryClient = useQueryClient()
const router = useRouter()
const { app, meta, ui } = useStore()
const { meta, ui } = useStore()
const { data: notifications, refetch } = useNotificationsQuery()
const [projectToRestart, setProjectToRestart] = useState<Project>()
@@ -82,7 +86,7 @@ const NotificationsPopover: FC<Props> = () => {
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<Props> = () => {
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<Props> = () => {
{}
)
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<Props> = () => {
{}
)
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}"`,
@@ -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<Props> = ({}) => {
const { app, ui, meta } = useStore()
const { meta } = useStore()
const { project } = useProjectContext()
const checkServerInterval = useRef<number>()
@@ -42,8 +43,13 @@ const RestoringState: FC<Props> = ({}) => {
}
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 (
@@ -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'
-2
View File
@@ -35,8 +35,6 @@ export const StoreProvider: FC<StoreProvider> = ({ children, rootStore }) => {
}, [isDarkMode, monaco])
useEffect(() => {
ui.load()
autorun(() => {
if (ui.notification) {
const { id, category, error, message, description, progress, duration } = ui.notification
+7 -9
View File
@@ -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<T>(
}
}, [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 <Error500 />
+10 -8
View File
@@ -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('/')
}
}
+25 -27
View File
@@ -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)
// }
// }
}
+39 -39
View File
@@ -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
+1 -129
View File
@@ -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]
}
}
}
-16
View File
@@ -1,16 +0,0 @@
import { Organization } from 'types'
import { IRootStore } from '../RootStore'
import PostgresMetaInterface from '../common/PostgresMetaInterface'
export default class OrganizationStore extends PostgresMetaInterface<Organization> {
constructor(
rootStore: IRootStore,
dataUrl: string,
headers?: {
[prop: string]: any
},
options?: { identifier: string }
) {
super(rootStore, dataUrl, headers, options)
}
}
-36
View File
@@ -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<Permission> {
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)
}
}
-96
View File
@@ -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<Project> {
fetchDetail: (projectRef: string, callback?: (project: Project) => void) => Promise<void>
fetchSubscriptionTier: (project: Project) => Promise<void>
}
export default class ProjectStore extends PostgresMetaInterface<Project> {
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
}
}
}
}