mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
commit
02ba582db6
25 files changed
+593
-300
No files matched your search
@@ -34,5 +34,9 @@
|
||||
{
|
||||
"question": "Can I self host Supabase for free?",
|
||||
"answer": "Yes, our Docker setup can be found [here](https://github.com/supabase/supabase), or in our [CLI](https://github.com/supabase/cli). Supabase Studio is available in the docker setup, read more about it [here](https://supabase.com/blog/2021/11/30/supabase-studio)."
|
||||
},
|
||||
{
|
||||
"question": "Can I pause a free project?",
|
||||
"answer": "Yes, you can pause a project at any time. Our Free tier gives you 2 free projects, but you can have as many paused projects as you want. Just pause and unpause them as needed."
|
||||
}
|
||||
]
|
||||
@@ -1,69 +1,65 @@
|
||||
import { FC } from 'react'
|
||||
import { Badge, IconLoader, IconPauseCircle } from '@supabase/ui'
|
||||
|
||||
import { Badge, Button, IconClock } from '@supabase/ui'
|
||||
import { Project } from 'types'
|
||||
import CardButton from 'components/ui/CardButton'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
paused: boolean
|
||||
onSelectRestore?: () => void
|
||||
onSelectDelete?: () => void
|
||||
rewriteHref?: string
|
||||
}
|
||||
|
||||
const ProjectCard: FC<Props> = ({
|
||||
project,
|
||||
paused,
|
||||
onSelectRestore,
|
||||
onSelectDelete,
|
||||
rewriteHref,
|
||||
}) => {
|
||||
const ProjectCard: FC<Props> = ({ project, rewriteHref }) => {
|
||||
const { name, ref: projectRef } = project
|
||||
const desc = `${project.cloud_provider} | ${project.region}`
|
||||
|
||||
const isPausing = project.status === PROJECT_STATUS.GOING_DOWN
|
||||
const isPaused = project.status === PROJECT_STATUS.INACTIVE
|
||||
const isRestoring = project.status === PROJECT_STATUS.RESTORING
|
||||
|
||||
return (
|
||||
<li className="col-span-1">
|
||||
<CardButton
|
||||
linkHref={rewriteHref ? rewriteHref : paused ? '' : `/project/${projectRef}`}
|
||||
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
|
||||
title={
|
||||
<div className="flex flex-row gap-1 w-full justify-between w-full">
|
||||
<span className="truncate flex-shrink">{name}</span>
|
||||
{paused && (
|
||||
<div className="grow text-right">
|
||||
<Badge color="scale">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconClock size={14} strokeWidth={2} />
|
||||
<span className="truncate">Project paused</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-row justify-between gap-1">
|
||||
<span className="flex-shrink truncate">{name}</span>
|
||||
</div>
|
||||
}
|
||||
description={''}
|
||||
footer={
|
||||
<div className="lowercase flex justify-between items-end">
|
||||
<span className="text-sm text-scale-900">{desc}</span>
|
||||
{paused && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
onClick={() => onSelectRestore && onSelectRestore()}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
onClick={() => onSelectDelete && onSelectDelete()}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<span className="text-scale-900 text-sm lowercase">{desc}</span>
|
||||
{isRestoring ? (
|
||||
<div className="grow text-right">
|
||||
<Badge color="brand">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconLoader className="animate-spin" size={14} strokeWidth={2} />
|
||||
<span className="truncate">Restoring</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
) : isPausing ? (
|
||||
<div className="grow text-right">
|
||||
<Badge color="scale">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconLoader className="animate-spin" size={14} strokeWidth={2} />
|
||||
<span className="truncate">Pausing</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
) : isPaused ? (
|
||||
<div className="grow text-right">
|
||||
<Badge color="scale">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconPauseCircle size={14} strokeWidth={2} />
|
||||
<span className="truncate">Paused</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
import { FC } from 'react'
|
||||
import { Button, IconLoader, IconPlus, Loading } from '@supabase/ui'
|
||||
import { Button, IconPlus } from '@supabase/ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { Organization, Project } from 'types'
|
||||
import { makeRandomString } from 'lib/helpers'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import ProjectCard from './ProjectCard'
|
||||
import ShimmeringCard from './ShimmeringCard'
|
||||
|
||||
interface Props {
|
||||
onSelectRestore: (project: Project) => void
|
||||
onSelectDelete: (project: Project) => void
|
||||
rewriteHref?: (projectRef: string) => string
|
||||
}
|
||||
|
||||
const ProjectList: FC<Props> = ({
|
||||
onSelectRestore = () => {},
|
||||
onSelectDelete = () => {},
|
||||
rewriteHref,
|
||||
}) => {
|
||||
const ProjectList: FC<Props> = ({ rewriteHref }) => {
|
||||
const router = useRouter()
|
||||
const { app } = useStore()
|
||||
const { organizations, projects } = app
|
||||
@@ -38,17 +31,17 @@ const ProjectList: FC<Props> = ({
|
||||
<div className="space-y-3" key={makeRandomString(5)}>
|
||||
<h4 className="text-lg">{name}</h4>
|
||||
{isLoadingProjects ? (
|
||||
<ul className="grid gap-4 mx-auto grid-cols-1 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 ">
|
||||
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 ">
|
||||
<ShimmeringCard />
|
||||
<ShimmeringCard />
|
||||
</ul>
|
||||
) : (
|
||||
<ul className="grid gap-4 mx-auto grid-cols-1 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 ">
|
||||
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 ">
|
||||
{!isLoadingProjects && isEmpty && (
|
||||
<div className="max-w-4xl text-center col-span-4 space-y-4 border-2 border-gray-300 border-dashed rounded-lg p-6">
|
||||
<div className="col-span-4 max-w-4xl space-y-4 rounded-lg border-2 border-dashed border-gray-300 p-6 text-center">
|
||||
<div className="space-y-1">
|
||||
<p>No projects</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Get started by creating a new project.
|
||||
</p>
|
||||
</div>
|
||||
@@ -63,10 +56,7 @@ const ProjectList: FC<Props> = ({
|
||||
<ProjectCard
|
||||
key={makeRandomString(5)}
|
||||
project={project}
|
||||
paused={project.status === PROJECT_STATUS.INACTIVE}
|
||||
rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
|
||||
onSelectDelete={() => onSelectDelete(project)}
|
||||
onSelectRestore={() => onSelectRestore(project)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -17,7 +17,7 @@ const FreeProjectLimitWarning: FC<Props> = ({ limit }) => {
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm leading-normal">
|
||||
{`Your account can only have up to ${limit} free projects - to create another free project, you'll need to delete an existing free project first. Otherwise, you may create a project on the Pro tier instead.`}
|
||||
{`Your account can only have up to ${limit} free projects - to create another free project, you'll need to delete or pause an existing free project first. Otherwise, you may create a project on the Pro tier instead.`}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Button } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { delete_ } from 'lib/common/fetch'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { Project } from 'types'
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
type?: 'danger' | 'default'
|
||||
}
|
||||
|
||||
const DeleteProjectButton: FC<Props> = ({ project, type = 'danger' }) => {
|
||||
const router = useRouter()
|
||||
const { ui, app } = useStore()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const toggle = () => {
|
||||
if (loading) return
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
async function handleDeleteProject() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await delete_(`${API_URL}/projects/${project.ref}`)
|
||||
if (response.error) throw response.error
|
||||
app.onProjectDeleted(response)
|
||||
ui.setNotification({ category: 'success', message: `Successfully deleted ${project.name}` })
|
||||
router.push(`/`)
|
||||
} catch (error: any) {
|
||||
setLoading(false)
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete project ${project.name}: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<Button onClick={toggle} type={type}>
|
||||
Delete project
|
||||
</Button>
|
||||
</div>
|
||||
<TextConfirmModal
|
||||
visible={isOpen}
|
||||
loading={loading}
|
||||
title={`Confirm deletion of ${project?.name}`}
|
||||
confirmPlaceholder="Type the project name in here"
|
||||
alert="This action cannot be undone."
|
||||
text={`This will permanently delete the ${project?.name} project and all of its data.`}
|
||||
confirmString={project?.name}
|
||||
confirmLabel="I understand, delete this project"
|
||||
onConfirm={handleDeleteProject}
|
||||
onCancel={toggle}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteProjectButton
|
||||
@@ -0,0 +1,65 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, IconPause } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
|
||||
interface Props {
|
||||
projectId: number
|
||||
projectRef: string
|
||||
}
|
||||
|
||||
const PauseProjectButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
const { ui, app } = useStore()
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const openModal = () => setIsModalOpen(true)
|
||||
const closeModal = () => setIsModalOpen(false)
|
||||
|
||||
const requestPauseProject = async () => {
|
||||
setLoading(true)
|
||||
const res = await post(`${API_URL}/projects/${projectRef}/pause`, {})
|
||||
|
||||
if (res.error) {
|
||||
ui.setNotification({
|
||||
error: res.error,
|
||||
category: 'error',
|
||||
message: 'Failed to pause project',
|
||||
})
|
||||
setLoading(false)
|
||||
} else {
|
||||
app.onProjectPaused(projectId)
|
||||
app.onProjectStatusUpdated(projectId, PROJECT_STATUS.GOING_DOWN)
|
||||
|
||||
ui.setNotification({ category: 'success', message: 'Pausing project' })
|
||||
router.push(`/project/${projectRef}`)
|
||||
}
|
||||
closeModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmModal
|
||||
danger
|
||||
visible={isModalOpen}
|
||||
title="Pause this project?"
|
||||
description="Are you sure you want to pause this project? It will not be accessible until you unpause it."
|
||||
buttonLabel="Pause project"
|
||||
buttonLoadingLabel="Pausing project"
|
||||
onSelectCancel={closeModal}
|
||||
onSelectConfirm={requestPauseProject}
|
||||
/>
|
||||
<Button type="default" icon={<IconPause />} onClick={openModal} loading={loading}>
|
||||
Pause Project
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default PauseProjectButton
|
||||
@@ -0,0 +1,63 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, IconRefreshCcw } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
|
||||
interface Props {
|
||||
projectId: number
|
||||
projectRef: string
|
||||
}
|
||||
|
||||
const RestartServerButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
const { ui, app } = useStore()
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const openModal = () => setIsModalOpen(true)
|
||||
const closeModal = () => setIsModalOpen(false)
|
||||
|
||||
const requestServerRestart = async () => {
|
||||
setLoading(true)
|
||||
const res = await post(`${API_URL}/projects/${projectRef}/restart`, {})
|
||||
|
||||
if (res.error) {
|
||||
ui.setNotification({
|
||||
error: res.error,
|
||||
category: 'error',
|
||||
message: 'Unable to restart server',
|
||||
})
|
||||
setLoading(false)
|
||||
} else {
|
||||
app.onProjectPostgrestStatusUpdated(projectId, 'OFFLINE')
|
||||
ui.setNotification({ category: 'success', message: 'Restarting server' })
|
||||
router.push(`/project/${projectRef}`)
|
||||
}
|
||||
closeModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmModal
|
||||
danger
|
||||
visible={isModalOpen}
|
||||
title="Restart Server"
|
||||
description={`Are you sure you want to restart the server? There will be a few minutes of downtime.`}
|
||||
buttonLabel="Restart"
|
||||
buttonLoadingLabel="Restarting"
|
||||
onSelectCancel={closeModal}
|
||||
onSelectConfirm={requestServerRestart}
|
||||
/>
|
||||
<Button type="default" icon={<IconRefreshCcw />} onClick={openModal} loading={loading}>
|
||||
Restart server
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default RestartServerButton
|
||||
@@ -0,0 +1,6 @@
|
||||
import PauseProjectButton from './PauseProjectButton'
|
||||
import RestartServerButton from './RestartServerButton'
|
||||
|
||||
import DeleteProjectButton from './DeleteProjectButton'
|
||||
|
||||
export { PauseProjectButton, RestartServerButton, DeleteProjectButton }
|
||||
@@ -62,12 +62,10 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
{/* Additional breadcrumbs are supplied */}
|
||||
<BreadcrumbsView defaultValue={breadcrumbs} />
|
||||
</div>
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
{customHeaderComponents && customHeaderComponents}
|
||||
<div className="flex items-center space-x-2">
|
||||
{IS_PLATFORM && <HelpPopover />}
|
||||
{IS_PLATFORM && <FeedbackDropdown />}
|
||||
</div>
|
||||
{IS_PLATFORM && <HelpPopover />}
|
||||
{IS_PLATFORM && <FeedbackDropdown />}
|
||||
{IS_PLATFORM && <NotificationsPopover />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FC } from 'react'
|
||||
import { Badge, IconLoader, IconCircle } from '@supabase/ui'
|
||||
import { Project } from 'types'
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
}
|
||||
|
||||
const PausingState: FC<Props> = ({ project }) => {
|
||||
return (
|
||||
<div className="mx-auto my-16 w-full max-w-7xl space-y-16">
|
||||
<div className="mx-6 space-y-16">
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-6">
|
||||
<h1 className="text-3xl">{project.name}</h1>
|
||||
<div>
|
||||
<Badge color="gray">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconLoader className="animate-spin" size={12} />
|
||||
<span>Pausing project</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mt-8 mb-16 w-full max-w-7xl">
|
||||
<div className="bg-scale-300 border-scale-400 mx-6 flex h-[500px] items-center justify-center rounded border p-8">
|
||||
<div className="grid w-[380px] gap-4">
|
||||
<div className="relative mx-auto max-w-[300px]">
|
||||
<div className="absolute flex h-full w-full items-center justify-center">
|
||||
<IconLoader className="animate-spin" size={20} strokeWidth={2} />
|
||||
</div>
|
||||
<IconCircle className="text-scale-900" size={50} strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="text-center">Pausing {project.name}</p>
|
||||
<p className="text-scale-1100 mt-4 text-center text-sm">
|
||||
You may restore your project anytime thereafter, and your data will be restored to
|
||||
when it was initially paused.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PausingState
|
||||
@@ -10,6 +10,7 @@ import NavigationBar from './NavigationBar/NavigationBar'
|
||||
import ProductMenuBar from './ProductMenuBar'
|
||||
import LayoutHeader from './LayoutHeader'
|
||||
import ConnectingState from './ConnectingState'
|
||||
import PausingState from './PausingState'
|
||||
import BuildingState from './BuildingState'
|
||||
|
||||
interface Props {
|
||||
@@ -110,13 +111,15 @@ const ContentWrapper: FC<ContentWrapperProps> = observer(({ isLoading, children
|
||||
const isProjectBuilding = [PROJECT_STATUS.COMING_UP, PROJECT_STATUS.RESTORING].includes(
|
||||
ui.selectedProject?.status ?? ''
|
||||
)
|
||||
|
||||
const isProjectPausing = ui.selectedProject?.status === PROJECT_STATUS.GOING_DOWN
|
||||
const isProjectOffline = ui.selectedProject?.postgrestStatus === 'OFFLINE'
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading || ui.selectedProject === undefined ? (
|
||||
<Connecting />
|
||||
) : isProjectPausing ? (
|
||||
<PausingState project={ui.selectedProject} />
|
||||
) : requiresPostgrestConnection && isProjectOffline ? (
|
||||
<ConnectingState project={ui.selectedProject} />
|
||||
) : requiresDbConnection && isProjectBuilding ? (
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { Alert, Button, IconPauseCircle } from '@supabase/ui'
|
||||
|
||||
import { Project } from 'types'
|
||||
import { useStore, useSubscriptionStats } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS, DEFAULT_FREE_PROJECTS_LIMIT } from 'lib/constants'
|
||||
import { DeleteProjectButton } from 'components/interfaces/Settings/General'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
}
|
||||
|
||||
const ProjectPausedState: FC<Props> = ({ project }) => {
|
||||
const { ui, app } = useStore()
|
||||
const subscriptionStats = useSubscriptionStats()
|
||||
const { total_active_free_projects: totalActiveFreeProjects } = subscriptionStats
|
||||
const freeProjectsLimit = ui.profile?.free_project_limit ?? DEFAULT_FREE_PROJECTS_LIMIT
|
||||
|
||||
const [showConfirmRestore, setShowConfirmRestore] = useState(false)
|
||||
const hasExceedActiveFreeProjectsLimit = totalActiveFreeProjects >= freeProjectsLimit
|
||||
|
||||
const onConfirmRestore = async () => {
|
||||
await post(`${API_URL}/projects/${project.ref}/restore`, {})
|
||||
app.onProjectUpdated({ ...project, status: PROJECT_STATUS.RESTORING })
|
||||
ui.setNotification({ category: 'success', message: 'Restoring project' })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{hasExceedActiveFreeProjectsLimit && (
|
||||
<div className="mx-6">
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
title={`Your account can only have ${freeProjectsLimit} active free projects`}
|
||||
>
|
||||
To restore this project you'll need to pause or delete an existing free project.
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<div className="mx-auto mb-16 w-full max-w-7xl">
|
||||
<div className="bg-scale-300 border-scale-400 mx-6 flex h-[500px] items-center justify-center rounded border p-8">
|
||||
<div className="grid w-[420px] gap-4">
|
||||
<div className="mx-auto flex max-w-[300px] items-center justify-center space-x-4 lg:space-x-8">
|
||||
<IconPauseCircle className="text-scale-1100" size={50} strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<p className="text-center">This project is paused.</p>
|
||||
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
disabled={hasExceedActiveFreeProjectsLimit}
|
||||
onClick={() => setShowConfirmRestore(true)}
|
||||
>
|
||||
Restore project
|
||||
</Button>
|
||||
<DeleteProjectButton project={project} type="default" />
|
||||
</div>
|
||||
|
||||
{!hasExceedActiveFreeProjectsLimit && (
|
||||
<p className="text-scale-1000 mt-4 text-sm">
|
||||
Restore this project and get back to building the next big thing!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
visible={showConfirmRestore}
|
||||
title="Restore this project"
|
||||
description="Confirm to restore this project? Your project's data will be restored to when it was initially paused."
|
||||
buttonLabel="Restore project"
|
||||
buttonLoadingLabel="Restoring project"
|
||||
onSelectCancel={() => setShowConfirmRestore(false)}
|
||||
onSelectConfirm={onConfirmRestore}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectPausedState
|
||||
@@ -27,7 +27,7 @@ const CardButton: FC<Props> = ({
|
||||
imgAlt,
|
||||
onClick,
|
||||
icon,
|
||||
containerHeightClassName="h-32"
|
||||
containerHeightClassName = 'h-32',
|
||||
}) => {
|
||||
const LinkContainer = ({ children }: { children: React.ReactNode }) => (
|
||||
<Link href={linkHref}>
|
||||
@@ -48,7 +48,7 @@ const CardButton: FC<Props> = ({
|
||||
'border border-panel-border-light dark:border-panel-border-dark',
|
||||
'rounded-md py-4 px-6 flex flex-row',
|
||||
'transition ease-in-out duration-150',
|
||||
containerHeightClassName
|
||||
containerHeightClassName,
|
||||
]
|
||||
|
||||
if (isLink) {
|
||||
@@ -62,7 +62,7 @@ const CardButton: FC<Props> = ({
|
||||
}
|
||||
|
||||
const ImageContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="flex flex-col mr-4">{children}</div>
|
||||
return <div className="mr-4 flex flex-col">{children}</div>
|
||||
}
|
||||
|
||||
const contents = (
|
||||
@@ -81,9 +81,9 @@ const CardButton: FC<Props> = ({
|
||||
</ImageContainer>
|
||||
)}
|
||||
{icon && <ImageContainer>{icon}</ImageContainer>}
|
||||
<div className="flex flex-col space-y-2 w-full h-full">
|
||||
<div className="flex h-full w-full flex-col space-y-2">
|
||||
<h5 className="text-scale-1200">{title}</h5>
|
||||
<div className="flex flex-col flex-1 w-full">
|
||||
<div className="flex w-full flex-1 flex-col">
|
||||
<p className="text-scale-1100 text-sm">{description}</p>
|
||||
<div className="w-full">{children && children}</div>
|
||||
</div>
|
||||
@@ -92,14 +92,14 @@ const CardButton: FC<Props> = ({
|
||||
{isLink && (
|
||||
<div
|
||||
className="
|
||||
text-scale-900
|
||||
group-hover:text-scale-1200
|
||||
absolute
|
||||
right-4
|
||||
top-3
|
||||
text-scale-900
|
||||
transition-all
|
||||
duration-200
|
||||
top-4
|
||||
transition-all
|
||||
duration-200
|
||||
group-hover:right-3
|
||||
group-hover:text-scale-1200
|
||||
"
|
||||
>
|
||||
<IconChevronRight />
|
||||
|
||||
@@ -5,6 +5,8 @@ import { API_URL } from 'lib/constants'
|
||||
export interface SubscriptionStats {
|
||||
total_paid_projects: number
|
||||
total_free_projects: number
|
||||
total_active_free_projects: number
|
||||
total_paused_free_projects: number
|
||||
total_pro_projects: number
|
||||
total_payg_projects: number
|
||||
}
|
||||
@@ -17,6 +19,8 @@ export function useSubscriptionStats() {
|
||||
return {
|
||||
total_paid_projects: data?.total_paid_projects ?? 0,
|
||||
total_free_projects: data?.total_free_projects ?? 0,
|
||||
total_active_free_projects: data?.total_active_free_projects ?? 0,
|
||||
total_paused_free_projects: data?.total_paused_free_projects ?? 0,
|
||||
total_pro_projects: data?.total_pro_projects ?? 0,
|
||||
total_payg_projects: data?.total_payg_projects ?? 0,
|
||||
isLoading: !anyError && !data,
|
||||
|
||||
+5
-63
@@ -1,56 +1,20 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { NextRouter, useRouter } from 'next/router'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { Typography } from '@supabase/ui'
|
||||
|
||||
import { NextPageWithLayout, Project } from 'types'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { auth } from 'lib/gotrue'
|
||||
import { post, delete_ } from 'lib/common/fetch'
|
||||
import { API_URL, IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
|
||||
import Connecting from 'components/ui/Loading'
|
||||
import { AccountLayoutWithoutAuth } from 'components/layouts'
|
||||
import Landing from 'components/interfaces/Home/Landing'
|
||||
import ProjectList from 'components/interfaces/Home/ProjectList'
|
||||
import OrganizationDropdown from 'components/to-be-cleaned/Dropdown/OrganizationDropdown'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
|
||||
const Home: NextPageWithLayout = () => {
|
||||
const { app, ui } = useStore()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const [isDeletingProject, setIsDeletingProject] = useState<boolean>(false)
|
||||
const [selectedProjectToDelete, setSelectedProjectToDelete] = useState<Project>()
|
||||
|
||||
const onSelectDeleteProject = async (project: Project) => {
|
||||
setSelectedProjectToDelete(project)
|
||||
setIsDeletingProject(false)
|
||||
}
|
||||
|
||||
const onDeleteProject = async (project?: Project) => {
|
||||
if (!project) return
|
||||
setIsDeletingProject(true)
|
||||
const response = await delete_(`${API_URL}/projects/${project.ref}`)
|
||||
|
||||
if (response.error) {
|
||||
return ui.setNotification({ category: 'error', message: response.error.message })
|
||||
}
|
||||
|
||||
app.onProjectDeleted(response)
|
||||
ui.setNotification({ category: 'success', message: `Deleted ${project.name} successfully!` })
|
||||
|
||||
setIsDeletingProject(false)
|
||||
setSelectedProjectToDelete(undefined)
|
||||
}
|
||||
|
||||
const onRestoreProject = async (project: Project) => {
|
||||
app.onProjectUpdated({ ...project, status: PROJECT_STATUS.RESTORING })
|
||||
await post(`${API_URL}/projects/${project.ref}/restore`, {})
|
||||
router.push(`/project/${project.ref}`)
|
||||
}
|
||||
const { app } = useStore()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -70,32 +34,10 @@ const Home: NextPageWithLayout = () => {
|
||||
</div>
|
||||
)}
|
||||
<div className="my-8 space-y-8">
|
||||
<ProjectList
|
||||
onSelectDelete={onSelectDeleteProject}
|
||||
onSelectRestore={onRestoreProject}
|
||||
/>
|
||||
<ProjectList />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<TextConfirmModal
|
||||
visible={!isUndefined(selectedProjectToDelete)}
|
||||
title="Are you absolutely sure?"
|
||||
alert="Deleting this project will also remove your database. Make sure you have made a backup if you want to keep your data."
|
||||
text={
|
||||
<span key="confirm-text">
|
||||
This action <Typography.Text strong>cannot</Typography.Text> be undone. This will
|
||||
permanently delete the{' '}
|
||||
<Typography.Text strong>{selectedProjectToDelete?.name}</Typography.Text> project and
|
||||
all of its data.
|
||||
</span>
|
||||
}
|
||||
loading={isDeletingProject}
|
||||
confirmLabel="Confirm delete"
|
||||
confirmPlaceholder="Type in name of project"
|
||||
confirmString={selectedProjectToDelete?.name ?? ''}
|
||||
onCancel={() => setSelectedProjectToDelete(undefined)}
|
||||
onConfirm={() => onDeleteProject(selectedProjectToDelete)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ const Wizard: NextPageWithLayout = () => {
|
||||
const currentOrg = organizations.find((o: any) => o.slug === slug)
|
||||
const stripeCustomerId = currentOrg?.stripe_customer_id
|
||||
|
||||
const totalFreeProjects = subscriptionStats.total_free_projects
|
||||
const totalFreeProjects = subscriptionStats.total_active_free_projects
|
||||
const freeProjectsLimit = ui.profile?.free_project_limit ?? DEFAULT_FREE_PROJECTS_LIMIT
|
||||
|
||||
const isOrganizationOwner = currentOrg?.is_owner || !app.organizations.isInitialized
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Typography } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { ProjectLayoutWithAuth } from 'components/layouts'
|
||||
import { ExampleProject, ClientLibrary } from 'components/interfaces/Home'
|
||||
import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { ProjectLayoutWithAuth } from 'components/layouts'
|
||||
import ProjectUsageSection from 'components/interfaces/Home/ProjectUsageSection'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import ProjectPausedState from 'components/layouts/ProjectLayout/ProjectPausedState'
|
||||
|
||||
const Home: NextPageWithLayout = () => {
|
||||
const { ui } = useStore()
|
||||
|
||||
const project = ui.selectedProject
|
||||
|
||||
const projectName =
|
||||
project?.ref !== 'default' && project?.name !== undefined
|
||||
? project?.name
|
||||
@@ -22,7 +24,11 @@ const Home: NextPageWithLayout = () => {
|
||||
<div className="mx-6 flex items-center space-x-6">
|
||||
<h1 className="text-3xl">{projectName}</h1>
|
||||
</div>
|
||||
{IS_PLATFORM && <ProjectUsageSection />}
|
||||
|
||||
{project?.status === PROJECT_STATUS.INACTIVE && <ProjectPausedState project={project} />}
|
||||
|
||||
{IS_PLATFORM && project?.status !== PROJECT_STATUS.INACTIVE && <ProjectUsageSection />}
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="mx-6">
|
||||
<Typography.Title level={4}>Client libraries</Typography.Title>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { toJS } from 'mobx'
|
||||
import { projects } from 'stores/jsonSchema'
|
||||
import { AutoField } from 'uniforms-bootstrap4'
|
||||
import { Alert, Button, Input, IconRefreshCcw } from '@supabase/ui'
|
||||
import { Alert, Input } from '@supabase/ui'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { pluckJsonSchemaFields, pluckObjectFields } from 'lib/helpers'
|
||||
import { post, delete_ } from 'lib/common/fetch'
|
||||
import { useStore } from 'hooks'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { pluckJsonSchemaFields, pluckObjectFields } from 'lib/helpers'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import {
|
||||
RestartServerButton,
|
||||
PauseProjectButton,
|
||||
DeleteProjectButton,
|
||||
} from 'components/interfaces/Settings/General'
|
||||
|
||||
import Panel from 'components/ui/Panel'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
|
||||
const ProjectSettings: NextPageWithLayout = () => {
|
||||
return (
|
||||
@@ -33,58 +35,16 @@ ProjectSettings.getLayout = (page) => <SettingsLayout title="General">{page}</Se
|
||||
|
||||
export default observer(ProjectSettings)
|
||||
|
||||
interface RestartServerButtonProps {
|
||||
projectId: number
|
||||
projectRef: string
|
||||
}
|
||||
const RestartServerButton: FC<RestartServerButtonProps> = observer(({ projectRef, projectId }) => {
|
||||
const { ui, app } = useStore()
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const openModal = () => setIsModalOpen(true)
|
||||
const closeModal = () => setIsModalOpen(false)
|
||||
|
||||
const requestServerRestart = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await post(`${API_URL}/projects/${projectRef}/restart`, {})
|
||||
app.onProjectPostgrestStatusUpdated(projectId, 'OFFLINE')
|
||||
ui.setNotification({ category: 'success', message: 'Restarting server' })
|
||||
router.push(`/project/${projectRef}`)
|
||||
} catch (error) {
|
||||
ui.setNotification({ error, category: 'error', message: 'Unable to restart server' })
|
||||
setLoading(false)
|
||||
}
|
||||
closeModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmModal
|
||||
danger
|
||||
visible={isModalOpen}
|
||||
title="Restart Server"
|
||||
description={`Are you sure you want to restart the server? There will be a few minutes of downtime.`}
|
||||
buttonLabel="Restart"
|
||||
buttonLoadingLabel="Restarting"
|
||||
onSelectCancel={closeModal}
|
||||
onSelectConfirm={requestServerRestart}
|
||||
/>
|
||||
<Button type="default" icon={<IconRefreshCcw />} onClick={openModal} loading={loading}>
|
||||
Restart server
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
const GeneralSettings = observer(() => {
|
||||
const { app, ui } = useStore()
|
||||
const isProjectPauseEnabled = useFlag('projectPausing')
|
||||
|
||||
const project = ui.selectedProject
|
||||
const formModel = toJS(project)
|
||||
const BASIC_FIELDS = ['name']
|
||||
|
||||
const isFreeProject = project?.subscription_tier === PRICING_TIER_PRODUCT_IDS.FREE
|
||||
|
||||
const handleUpdateProject = async (model: any) => {
|
||||
const response = await post(`${API_URL}/projects/${project?.ref}/update`, model)
|
||||
if (response.error) {
|
||||
@@ -136,8 +96,8 @@ const GeneralSettings = observer(() => {
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<p>Restart server</p>
|
||||
<div style={{ maxWidth: '420px' }}>
|
||||
<p className="text-sm opacity-50">
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Your project will not be available for a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
@@ -145,79 +105,44 @@ const GeneralSettings = observer(() => {
|
||||
{project && <RestartServerButton projectId={project.id} projectRef={project.ref} />}
|
||||
</div>
|
||||
</Panel.Content>
|
||||
{isProjectPauseEnabled && isFreeProject && (
|
||||
<Panel.Content className="border-panel-border-interior-light dark:border-panel-border-interior-dark border-t">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<p>Pause project</p>
|
||||
<div className="max-w-[420px]">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Your project will not be accessible while it is paused.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{project && <PauseProjectButton projectId={project.id} projectRef={project.ref} />}
|
||||
</div>
|
||||
</Panel.Content>
|
||||
)}
|
||||
</Panel>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Panel title={<p className="uppercase">Danger Zone</p>}>
|
||||
<Panel.Content>
|
||||
<Alert
|
||||
variant="danger"
|
||||
withIcon
|
||||
title="Deleting this project will also remove your database."
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<p className="mb-4 block">
|
||||
Make sure you have made a backup if you want to keep your data.
|
||||
</p>
|
||||
<ProjectDeleteModal project={project} />
|
||||
</div>
|
||||
</Alert>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
{project !== undefined && (
|
||||
<section>
|
||||
<Panel title={<p className="uppercase">Danger Zone</p>}>
|
||||
<Panel.Content>
|
||||
<Alert
|
||||
variant="danger"
|
||||
withIcon
|
||||
title="Deleting this project will also remove your database."
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<p className="mb-4 block">
|
||||
Make sure you have made a backup if you want to keep your data.
|
||||
</p>
|
||||
<DeleteProjectButton project={project} />
|
||||
</div>
|
||||
</Alert>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
})
|
||||
|
||||
const ProjectDeleteModal = ({ project }: any) => {
|
||||
const router = useRouter()
|
||||
const { ui, app } = useStore()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const toggle = () => {
|
||||
if (loading) return
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
async function handleDeleteProject() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await delete_(`${API_URL}/projects/${project.ref}`)
|
||||
if (response.error) throw response.error
|
||||
app.onProjectDeleted(response)
|
||||
ui.setNotification({ category: 'success', message: `Successfully deleted ${project.name}` })
|
||||
router.push(`/`)
|
||||
} catch (error: any) {
|
||||
setLoading(false)
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to delete project ${project.name}: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-2">
|
||||
<Button onClick={toggle} type="danger">
|
||||
Delete Project
|
||||
</Button>
|
||||
</div>
|
||||
<TextConfirmModal
|
||||
visible={isOpen}
|
||||
loading={loading}
|
||||
title={`Confirm deletion of ${project?.name}`}
|
||||
confirmPlaceholder="Type the project name in here"
|
||||
alert="This action cannot be undone."
|
||||
text={`This will permanently delete the ${project?.name} project and all of its data.`}
|
||||
confirmString={project?.name}
|
||||
confirmLabel="I understand, delete this project"
|
||||
onConfirm={handleDeleteProject}
|
||||
onCancel={toggle}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -11,14 +11,14 @@ interface Props {}
|
||||
|
||||
const Header: FC<Props> = () => {
|
||||
return (
|
||||
<div className="p-3 border-b dark:border-dark">
|
||||
<div className="space-x-2 flex items-center">
|
||||
<div className="dark:border-dark border-b p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="/">
|
||||
<a>
|
||||
<img
|
||||
src="/img/supabase-logo.svg"
|
||||
alt="Supabase"
|
||||
className="border dark:border-dark rounded p-1 hover:border-white"
|
||||
className="dark:border-dark rounded border p-1 hover:border-white"
|
||||
style={{ height: 24 }}
|
||||
/>
|
||||
</a>
|
||||
@@ -50,14 +50,10 @@ const GenericProjectPage: NextPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<div className="py-8 w-full max-w-5xl mx-auto">
|
||||
<div className="mx-auto w-full max-w-5xl py-8">
|
||||
<h3 className="text-2xl">Select a project to continue</h3>
|
||||
<div className="my-6 space-y-8">
|
||||
<ProjectList
|
||||
rewriteHref={urlRewriterFactory(routeSlug)}
|
||||
onSelectDelete={() => {}}
|
||||
onSelectRestore={() => {}}
|
||||
/>
|
||||
<ProjectList rewriteHref={urlRewriterFactory(routeSlug)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { cloneDeep } from 'lodash'
|
||||
import { values } from 'mobx'
|
||||
import { Organization, Project } from 'types'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { getWithTimeout } from 'lib/common/fetch'
|
||||
import { IRootStore } from '../RootStore'
|
||||
import DatabaseStore, { IDatabaseStore } from './DatabaseStore'
|
||||
import OrganizationStore from './OrganizationStore'
|
||||
@@ -12,9 +13,10 @@ export interface IAppStore {
|
||||
projects: IProjectStore
|
||||
organizations: OrganizationStore
|
||||
database: IDatabaseStore
|
||||
onProjectCreated: (project: any) => void
|
||||
onProjectUpdated: (project: any) => void
|
||||
onProjectDeleted: (project: any) => void
|
||||
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
|
||||
@@ -76,6 +78,24 @@ export default class AppStore implements IAppStore {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { cloneDeep, keyBy } from 'lodash'
|
||||
import { Project } from 'types'
|
||||
import { IRootStore } from '../RootStore'
|
||||
import { constructHeaders } from 'lib/api/apiHelpers'
|
||||
@@ -24,6 +24,26 @@ export default class ProjectStore extends PostgresMetaInterface<Project> {
|
||||
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) {
|
||||
const url = `${this.url}/${projectRef}`
|
||||
const headers = constructHeaders(this.headers)
|
||||
|
||||
+3
-4
@@ -2,13 +2,12 @@
|
||||
|
||||
### Contributing to the specs
|
||||
|
||||
- `cd /web/spec/`
|
||||
- `cd ./web/spec/`
|
||||
- run `npm install`
|
||||
- run `make` which will pull the latest TSDoc with your updates
|
||||
|
||||
> Note: `make` requires [`jq - a lightweight and flexible command-line JSON processor`](https://stedolan.github.io/jq/). To install on OSX with homebrew, run: `brew install jq`.
|
||||
|
||||
- Add your comments to `supabase.yml` | `dart.yml` | `postgrest.yml` | `gotrue.yml` | `realtime.yml`
|
||||
- run `npm run gen:supabase` inside the `/spec` folder which will generate the Docusaurus pages
|
||||
- run `npm run build` to make sure the build is working
|
||||
- Apply your updates to `supabase.yml` | `dart.yml` | `postgrest.yml` | `gotrue.yml` | `realtime.yml`
|
||||
- run `npm run build && npm run start` to make sure the build is working
|
||||
- Submit your PR
|
||||
@@ -86,7 +86,7 @@ select hello_world();
|
||||
<TabItem value="JS">
|
||||
|
||||
```js
|
||||
const { data, error } = supabase
|
||||
const { data, error } = await supabase
|
||||
.rpc('hello_world')
|
||||
```
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Command reference',
|
||||
items: ['reference/cli/supabase-help', 'reference/cli/supabase-login', 'reference/cli/supabase-link', 'reference/cli/supabase-init', 'reference/cli/supabase-start', 'reference/cli/supabase-stop', 'reference/cli/supabase-db-branch-list', 'reference/cli/supabase-db-branch-create', 'reference/cli/supabase-db-branch-delete', 'reference/cli/supabase-db-switch', 'reference/cli/supabase-db-changes', 'reference/cli/supabase-db-commit', 'reference/cli/supabase-db-reset', 'reference/cli/supabase-db-remote-set', 'reference/cli/supabase-db-remote-commit', 'reference/cli/supabase-db-push', 'reference/cli/supabase-functions-delete', 'reference/cli/supabase-functions-deploy', 'reference/cli/supabase-functions-new', 'reference/cli/supabase-functions-serve', 'reference/cli/supabase-migration-new', 'reference/cli/supabase-secrets-list', 'reference/cli/supabase-secrets-set', 'reference/cli/supabase-secrets-unset'],
|
||||
label: 'Command Reference',
|
||||
items: ['reference/cli/supabase-help', 'reference/cli/supabase-login', 'reference/cli/supabase-link', 'reference/cli/supabase-init', 'reference/cli/supabase-start', 'reference/cli/supabase-stop', 'reference/cli/supabase-db-branch-list', 'reference/cli/supabase-db-branch-create', 'reference/cli/supabase-db-branch-delete', 'reference/cli/supabase-db-switch', 'reference/cli/supabase-db-changes', 'reference/cli/supabase-db-commit', 'reference/cli/supabase-db-reset', 'reference/cli/supabase-db-remote-set', 'reference/cli/supabase-db-remote-commit', 'reference/cli/supabase-db-push', 'reference/cli/supabase-functions-delete', 'reference/cli/supabase-functions-deploy', 'reference/cli/supabase-functions-new', 'reference/cli/supabase-functions-serve', 'reference/cli/supabase-migration-new', 'reference/cli/supabase-orgs-list', 'reference/cli/supabase-projects-list', 'reference/cli/supabase-projects-create', 'reference/cli/supabase-secrets-list', 'reference/cli/supabase-secrets-set', 'reference/cli/supabase-secrets-unset'],
|
||||
collapsed: true,
|
||||
}
|
||||
],
|
||||
|
||||
+58
-3
@@ -39,8 +39,8 @@ info:
|
||||
items:
|
||||
- index
|
||||
- Installing and Updating
|
||||
- Config reference
|
||||
- name: 'Command reference'
|
||||
- Config Reference
|
||||
- name: 'Command Reference'
|
||||
items:
|
||||
- supabase help
|
||||
- supabase login
|
||||
@@ -63,6 +63,9 @@ info:
|
||||
- supabase functions new
|
||||
- supabase functions serve
|
||||
- supabase migration new
|
||||
- supabase orgs list
|
||||
- supabase projects list
|
||||
- supabase projects create
|
||||
- supabase secrets list
|
||||
- supabase secrets set
|
||||
- supabase secrets unset
|
||||
@@ -315,6 +318,58 @@ pages:
|
||||
supabase migration new <migration name>
|
||||
```
|
||||
|
||||
supabase orgs list:
|
||||
description: |
|
||||
List all organizations.
|
||||
examples:
|
||||
- name: Usage
|
||||
sh: |
|
||||
```sh
|
||||
supabase orgs list
|
||||
```
|
||||
|
||||
supabase projects list:
|
||||
description: |
|
||||
List all projects.
|
||||
examples:
|
||||
- name: Usage
|
||||
sh: |
|
||||
```sh
|
||||
supabase projects list
|
||||
```
|
||||
|
||||
supabase projects create:
|
||||
description: |
|
||||
Create a new project.
|
||||
|
||||
Flags:
|
||||
|
||||
`--org-id`: Organization ID to create the project in.
|
||||
|
||||
`--db-password`: Database password of the project.
|
||||
|
||||
`--region`: Select a region close to you for the best performance. Allowed values:
|
||||
- `ap-northeast-1`
|
||||
- `ap-northeast-2`
|
||||
- `ap-south-1`
|
||||
- `ap-southeast-1`
|
||||
- `ap-southeast-2`
|
||||
- `ca-central-1`
|
||||
- `eu-central-1`
|
||||
- `eu-west-1`
|
||||
- `eu-west-2`
|
||||
- `sa-east-1`
|
||||
- `us-east-1`
|
||||
- `us-west-1`
|
||||
|
||||
`--plan`: Select a plan that suits your needs. Allowed values: `free`, `pro`.
|
||||
examples:
|
||||
- name: Usage
|
||||
sh: |
|
||||
```sh
|
||||
supabase projects create my-project --org-id cool-green-pqdr0qc --db-password ******** --region us-east-1
|
||||
```
|
||||
|
||||
supabase secrets list:
|
||||
description: |
|
||||
List all secrets in the linked project.
|
||||
@@ -416,7 +471,7 @@ pages:
|
||||
- `sudo dpkg -i <...>.deb`
|
||||
- `sudo rpm -i <...>.rpm`
|
||||
|
||||
Config reference:
|
||||
Config Reference:
|
||||
description: |
|
||||
A `config.toml` file is generated after running `supabase init`.
|
||||
|
||||
|
||||
Reference in new issue
Block a user