mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
delete org and project stores
This commit is contained in:
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 {
|
||||
|
||||
+13
-9
@@ -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}`)
|
||||
|
||||
+11
-8
@@ -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
|
||||
+26
-16
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user