Merge branch 'master' of github.com:supabase/supabase

This commit is contained in:
Terry Sutton committed 2022-06-30 09:35:06 -02:30
commit 02ba582db6
25 files changed
+593 -300

No files matched your search

+4
View File
@@ -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
+10 -10
View File
@@ -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
View File
@@ -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)}
/>
</>
)
}
+1 -1
View File
@@ -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
+11 -5
View File
@@ -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>
+55 -130
View File
@@ -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}
/>
</>
)
}
+5 -9
View File
@@ -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>
+24 -4
View File
@@ -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
+21 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -86,7 +86,7 @@ select hello_world();
<TabItem value="JS">
```js
const { data, error } = supabase
const { data, error } = await supabase
.rpc('hello_world')
```
+2 -2
View File
@@ -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
View File
@@ -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`.