mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 23:59:08 +03:00
43 files changed
+619
-355
No files matched your search
@@ -8,41 +8,42 @@ import { Organization, Project } from 'types'
|
||||
// Ideally these could all be within a _middleware when we use Next 12
|
||||
const RouteValidationWrapper: FC = ({ children }) => {
|
||||
const { ui, app } = useStore()
|
||||
const userProfile = ui.profile
|
||||
const orgsInitialized = app.organizations.isInitialized
|
||||
const projectsInitialized = app.projects.isInitialized
|
||||
|
||||
const router = useRouter()
|
||||
const projectRef = router.query.ref
|
||||
const orgSlug = router.query.slug
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile) {
|
||||
if (orgsInitialized && orgSlug) {
|
||||
// Check validity of organization that user is trying to access
|
||||
if (orgSlug) {
|
||||
const organizations = app.organizations.list()
|
||||
const organizationSlugs = organizations.map((org: Organization) => org.slug)
|
||||
const isValidOrg = organizationSlugs.includes(orgSlug as string)
|
||||
const organizations = app.organizations.list()
|
||||
const organizationSlugs = organizations.map((org: Organization) => org.slug)
|
||||
const isValidOrg = organizationSlugs.includes(orgSlug as string)
|
||||
|
||||
if (!isValidOrg) {
|
||||
ui.setNotification({ category: 'error', message: 'This organization does not exist' })
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check validity of project that the user is trying to access
|
||||
if (projectRef) {
|
||||
const projects = app.projects.list()
|
||||
const projectRefs = projects.map((project: Project) => project.ref)
|
||||
const isValidProject = projectRefs.includes(projectRef as string)
|
||||
|
||||
if (!isValidProject) {
|
||||
ui.setNotification({ category: 'error', message: 'This project does not exist' })
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
if (!isValidOrg) {
|
||||
ui.setNotification({ category: 'error', message: 'This organization does not exist' })
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
}
|
||||
}, [userProfile])
|
||||
}, [orgsInitialized])
|
||||
|
||||
useEffect(() => {
|
||||
if (projectsInitialized && projectRef) {
|
||||
// Check validity of project that the user is trying to access
|
||||
const projects = app.projects.list()
|
||||
const projectRefs = projects.map((project: Project) => project.ref)
|
||||
const isValidProject = projectRefs.includes(projectRef as string)
|
||||
|
||||
if (!isValidProject) {
|
||||
ui.setNotification({ category: 'error', message: 'This project does not exist' })
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
}
|
||||
}, [projectsInitialized])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Link from 'next/link'
|
||||
import React, { FC, useState, useEffect } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Typography, IconClock, IconInfo } from '@supabase/ui'
|
||||
import { Typography, IconInfo } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import BackupItem from './BackupItem'
|
||||
import Loading from 'components/ui/Loading'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
|
||||
interface Props {}
|
||||
@@ -44,32 +44,11 @@ const BackupsList: FC<Props> = ({}) => {
|
||||
|
||||
if (tierId === STRIPE_PRODUCT_IDS.FREE) {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'block w-full py-4 px-6 border rounded border-opacity-20',
|
||||
'bg-gray-100 border-gray-600',
|
||||
'dark:bg-gray-400 dark:border-gray-300',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
<div className="mt-1">
|
||||
<IconClock size="large" />
|
||||
</div>
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div>
|
||||
<p>Free Plan does not include project backups.</p>
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Please upgrade to Pro plan for up to 7 days of backups.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/project/${projectRef}/settings/billing`}>
|
||||
<Button type="primary">Upgrade to Pro</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UpgradeToPro
|
||||
primaryText="Free Plan does not include project backups."
|
||||
projectRef={projectRef}
|
||||
secondaryText="Please upgrade to Pro plan for up to 7 days of backups."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,21 +27,23 @@ const ProjectCard: FC<Props> = ({
|
||||
<CardButton
|
||||
linkHref={rewriteHref ? rewriteHref : paused ? '' : `/project/${projectRef}`}
|
||||
title={
|
||||
<div className="flex items-center gap-3 w-full">
|
||||
<span>{name}</span>
|
||||
<div className="flex flex-row gap-1 w-full justify-between w-full">
|
||||
<span className="truncate flex-shrink">{name}</span>
|
||||
{paused && (
|
||||
<Badge color="scale">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconClock size={14} strokeWidth={2} />
|
||||
<span>Project paused</span>
|
||||
</div>
|
||||
</Badge>
|
||||
<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>
|
||||
}
|
||||
description={''}
|
||||
footer={
|
||||
<div className="lowercase flex items-center justify-between">
|
||||
<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">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react'
|
||||
import { Button, IconPlus, Loading, Typography } from '@supabase/ui'
|
||||
import { Button, IconLoader, IconPlus, Loading } from '@supabase/ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Organization, Project } from 'types'
|
||||
import { makeRandomString } from 'lib/helpers'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import ProjectCard from './ProjectCard'
|
||||
import PausedProjectCard from './PausedProjectCard'
|
||||
|
||||
interface Props {
|
||||
onSelectRestore: (project: Project) => void
|
||||
@@ -24,7 +23,7 @@ const ProjectList: FC<Props> = ({
|
||||
const router = useRouter()
|
||||
const { app } = useStore()
|
||||
const { organizations, projects } = app
|
||||
const { isLoading } = projects
|
||||
const { isLoading: isLoadingProjects } = projects
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -36,35 +35,36 @@ const ProjectList: FC<Props> = ({
|
||||
const isEmpty = sortedProjects?.length == 0
|
||||
return (
|
||||
<div className="space-y-3" key={makeRandomString(5)}>
|
||||
<Loading active={isLoading}>
|
||||
<h4 className="text-lg">{name}</h4>
|
||||
<h4 className="text-lg">{name}</h4>
|
||||
<Loading active={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 ">
|
||||
{!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="space-y-1">
|
||||
<p>No projects</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Get started by creating a new project.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={() => router.push(`/new/${slug}`)} icon={<IconPlus />}>
|
||||
New Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{sortedProjects?.map((project: Project) => (
|
||||
<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>
|
||||
</Loading>
|
||||
<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 ">
|
||||
{!isLoading && 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="space-y-1">
|
||||
<p>No projects</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Get started by creating a new project.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={() => router.push(`/new/${slug}`)} icon={<IconPlus />}>
|
||||
New Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{sortedProjects?.map((project: Project) => (
|
||||
<ProjectCard
|
||||
paused={project.status === PROJECT_STATUS.INACTIVE}
|
||||
key={makeRandomString(5)}
|
||||
project={project}
|
||||
onSelectDelete={() => onSelectDelete(project)}
|
||||
onSelectRestore={() => onSelectRestore(project)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import useSWR from 'swr'
|
||||
import { FC } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Typography, IconLoader } from '@supabase/ui'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import { ProjectUsage, NewProjectPanel } from 'components/interfaces/Home'
|
||||
|
||||
const ProjectUsageSection: FC = observer(({}) => {
|
||||
const { ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
const { data: usage, error: usageError }: any = useSWR(
|
||||
`${API_URL}/projects/${project?.ref}/usage`,
|
||||
get
|
||||
)
|
||||
const hasProjectData =
|
||||
usage && (usage?.bucketSize || (usage?.authUsers ?? '0') !== '0' || usage?.dbTables)
|
||||
? true
|
||||
: false
|
||||
|
||||
if (usageError) {
|
||||
return <Typography.Text type="danger">Error loading data {usageError.message}</Typography.Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{usage === undefined ? (
|
||||
<div className="w-full flex justify-center items-center space-x-2">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<p className="text-sm">Retrieving project usage statistics</p>
|
||||
</div>
|
||||
) : !usage.error && hasProjectData ? (
|
||||
<ProjectUsage project={project} />
|
||||
) : (
|
||||
<NewProjectPanel />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
export default ProjectUsageSection
|
||||
@@ -93,6 +93,39 @@ language sql volatile;
|
||||
-- const { data, error } = await supabase.rpc('increment', { row_id: 2 })
|
||||
`.trim(),
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
type: 'template',
|
||||
title: 'pg_stat_statements report',
|
||||
description: 'Select from pg_stat_statements and view recent queries',
|
||||
sql: `-- pg_stat_statements report
|
||||
|
||||
-- A limit of 100 has been added below
|
||||
|
||||
SELECT
|
||||
a.rolname,
|
||||
s.query,
|
||||
s.calls,
|
||||
-- -- Postgres 13, 14
|
||||
s.total_exec_time + s.total_plan_time as total_time,
|
||||
s.min_exec_time + s.min_plan_time as min_time,
|
||||
s.max_exec_time + s.max_plan_time as max_time,
|
||||
s.mean_exec_time + s.mean_plan_time as mean_time,
|
||||
-- -- Postgres <= 12
|
||||
-- total_time,
|
||||
-- min_time,
|
||||
-- max_time,
|
||||
-- mean_time,
|
||||
s.rows / s.calls as avg_rows,
|
||||
s.wal_bytes,
|
||||
s.wal_records
|
||||
from pg_stat_statements as s
|
||||
join pg_authid as a on s.userid = a.oid
|
||||
ORDER BY
|
||||
total_time DESC
|
||||
LIMIT
|
||||
100`,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
type: 'quickstart',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { FC } from 'react'
|
||||
import { Input, Typography, IconX } from '@supabase/ui'
|
||||
import { Typography, IconX, Input } from '@supabase/ui'
|
||||
|
||||
import { LogData } from './Logs.types'
|
||||
|
||||
@@ -39,18 +39,25 @@ const LogSelection: FC<Props> = ({ log, onClose }) => {
|
||||
<div className="flex-grow overflow-y-auto bg-panel-body-light dark:bg-panel-body-dark">
|
||||
<div className="p-4 flex-col space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Typography.Text small>Event message</Typography.Text>
|
||||
<Typography.Text code className="block w-full overflow-x-auto" small>
|
||||
{log.event_message}{' '}
|
||||
</Typography.Text>
|
||||
<p className="text-sm">Event message</p>
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
size="small"
|
||||
layout="vertical"
|
||||
className="text-sm input-mono"
|
||||
value={log.event_message}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Typography.Text small>Metadata</Typography.Text>
|
||||
<p className="text-sm">Metadata</p>
|
||||
<Input.TextArea
|
||||
className="font-mono mt-4"
|
||||
size="tiny"
|
||||
rows={25}
|
||||
disabled
|
||||
copy
|
||||
readOnly
|
||||
size="small"
|
||||
layout="vertical"
|
||||
rows={20}
|
||||
className="input-mono"
|
||||
value={JSON.stringify(log.metadata, null, 2)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@ const Column: FC<Props> = ({
|
||||
} rounded-md`}
|
||||
actions={
|
||||
<Button
|
||||
type={!isUndefined(column.foreignKey) ? 'default' : 'outline'}
|
||||
type={!isUndefined(column.foreignKey) ? 'secondary' : 'default'}
|
||||
onClick={() => onEditRelation(column)}
|
||||
>
|
||||
<IconLink size={14} strokeWidth={!isUndefined(column.foreignKey) ? 2 : 1} />
|
||||
@@ -216,7 +216,7 @@ const Column: FC<Props> = ({
|
||||
{settingsCount}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-scale-900 transition-colors group-hover:text-scale-1200">
|
||||
<div className="text-scale-1100 transition-colors group-hover:text-scale-1200">
|
||||
<IconSettings size={18} strokeWidth={1} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,8 +24,10 @@ const AuthLayout: FC<Props> = ({ title, children }) => {
|
||||
const [loaded, setLoaded] = useState<boolean>(isInitialized)
|
||||
|
||||
useEffect(() => {
|
||||
meta.tables.load()
|
||||
}, [])
|
||||
if (ui.selectedProject) {
|
||||
meta.tables.load()
|
||||
}
|
||||
}, [ui.selectedProject])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !loaded) {
|
||||
|
||||
@@ -24,15 +24,17 @@ const DatabaseLayout: FC<Props> = ({ title, children }) => {
|
||||
const [loaded, setLoaded] = useState<boolean>(isInitialized)
|
||||
|
||||
useEffect(() => {
|
||||
// Eventually should only load the required stores based on the pages
|
||||
meta.schemas.load()
|
||||
meta.tables.load()
|
||||
if (ui.selectedProject) {
|
||||
// Eventually should only load the required stores based on the pages
|
||||
meta.schemas.load()
|
||||
meta.tables.load()
|
||||
|
||||
meta.roles.load()
|
||||
meta.triggers.load()
|
||||
meta.extensions.load()
|
||||
meta.publications.load()
|
||||
}, [])
|
||||
meta.roles.load()
|
||||
meta.triggers.load()
|
||||
meta.extensions.load()
|
||||
meta.publications.load()
|
||||
}
|
||||
}, [ui.selectedProject])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !loaded) {
|
||||
|
||||
@@ -12,8 +12,10 @@ function DocsLayout({ title, children }: { title: string; children: ReactElement
|
||||
const { data, isLoading, error } = meta.openApi
|
||||
|
||||
useEffect(() => {
|
||||
meta.openApi.load()
|
||||
}, [])
|
||||
if (ui.selectedProject) {
|
||||
meta.openApi.load()
|
||||
}
|
||||
}, [ui.selectedProject])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@ const FeedbackDropdown = () => {
|
||||
type="default"
|
||||
icon={<IconMessageCircle size={16} strokeWidth={1.5} className="text-scale-900" />}
|
||||
>
|
||||
Feedback on this page?
|
||||
<span className="block md:hidden">Feedback</span>
|
||||
<span className="hidden md:block">Feedback on this page?</span>
|
||||
</Button>
|
||||
</Popover>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import Head from 'next/head'
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Project } from 'types'
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
import Connecting from 'components/ui/Loading'
|
||||
@@ -32,34 +29,6 @@ const ProjectLayout: FC<Props> = ({
|
||||
hideHeader = false,
|
||||
hideIconBar = false,
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
const project: Project | undefined = ui.selectedProject
|
||||
|
||||
const router = useRouter()
|
||||
const requiresDbConnection: boolean = router.pathname !== '/project/[ref]/settings/general'
|
||||
|
||||
// useEffect(() => {
|
||||
// if (isUndefined(project)) router.push('/404')
|
||||
// }, [])
|
||||
|
||||
const renderContent = () => {
|
||||
if (!isUndefined(project)) {
|
||||
return requiresDbConnection ? (
|
||||
<TestConnection project={project}>
|
||||
{isLoading ? (
|
||||
<Connecting />
|
||||
) : (
|
||||
<div className="flex flex-col flex-1 overflow-y-auto">{children}</div>
|
||||
)}
|
||||
</TestConnection>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)
|
||||
} else {
|
||||
return <>{children}</>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -72,16 +41,16 @@ const ProjectLayout: FC<Props> = ({
|
||||
{!hideIconBar && <NavigationBar />}
|
||||
|
||||
{/* Product menu bar */}
|
||||
{productMenu && !isLoading && (
|
||||
<MenuBarWrapper isLoading={isLoading} productMenu={productMenu}>
|
||||
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
|
||||
)}
|
||||
</MenuBarWrapper>
|
||||
|
||||
<main
|
||||
style={{ maxHeight: '100vh' }}
|
||||
className="w-full flex flex-col flex-1 overflow-x-hidden"
|
||||
>
|
||||
{!hideHeader && <LayoutHeader />}
|
||||
{renderContent()}
|
||||
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
@@ -89,3 +58,38 @@ const ProjectLayout: FC<Props> = ({
|
||||
}
|
||||
|
||||
export default observer(ProjectLayout)
|
||||
|
||||
interface MenuBarWrapperProps {
|
||||
isLoading: boolean
|
||||
productMenu?: ReactNode
|
||||
}
|
||||
|
||||
const MenuBarWrapper: FC<MenuBarWrapperProps> = observer(({ isLoading, productMenu, children }) => {
|
||||
const { ui } = useStore()
|
||||
return <>{!isLoading && productMenu && ui.selectedProject !== undefined ? children : null}</>
|
||||
})
|
||||
|
||||
interface ContentWrapperProps {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const ContentWrapper: FC<ContentWrapperProps> = observer(({ isLoading, children }) => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
const project = ui.selectedProject
|
||||
const requiresDbConnection: boolean = router.pathname !== '/project/[ref]/settings/general'
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading || project === undefined ? (
|
||||
<Connecting />
|
||||
) : requiresDbConnection ? (
|
||||
<TestConnection project={project!}>
|
||||
<div className="flex flex-col flex-1 overflow-y-auto">{children}</div>
|
||||
</TestConnection>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -14,31 +14,18 @@ interface Props {
|
||||
|
||||
const SQLEditorLayout: FC<Props> = ({ title, children }) => {
|
||||
const { content } = useStore()
|
||||
const { isInitialized, isLoading, error } = content
|
||||
|
||||
const [loaded, setLoaded] = useState(isInitialized)
|
||||
|
||||
useEffect(() => {
|
||||
content.load()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !loaded) {
|
||||
setLoaded(true)
|
||||
}
|
||||
}, [isLoading])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ProjectLayout>
|
||||
<Error error={error} />
|
||||
</ProjectLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
return content.error ? (
|
||||
<ProjectLayout>
|
||||
<Error error={content.error} />
|
||||
</ProjectLayout>
|
||||
) : (
|
||||
<ProjectLayout
|
||||
isLoading={!loaded}
|
||||
isLoading={content.isLoading}
|
||||
title={title || 'SQL'}
|
||||
product="SQL Editor"
|
||||
productMenu={<SQLEditorMenuOld />}
|
||||
|
||||
@@ -11,7 +11,14 @@ export const generateSettingsMenu = (ref: string): ProductMenuGroup[] => {
|
||||
{ name: 'General', key: 'general', url: `/project/${ref}/settings/general`, items: [] },
|
||||
{ name: 'Database', key: 'database', url: `/project/${ref}/settings/database`, items: [] },
|
||||
{ name: 'API', key: 'api', url: `/project/${ref}/settings/api`, items: [] },
|
||||
{ name: 'Auth Settings', key: 'auth', url: `/project/${ref}/auth/settings`, items: [] },
|
||||
{ name: 'Authentication', key: 'auth', url: `/project/${ref}/auth/settings`, items: [] },
|
||||
// TODO(thebengeu): Uncomment once all tenants migrated to multitenant storage API.
|
||||
// {
|
||||
// name: 'Storage',
|
||||
// key: 'storage',
|
||||
// url: `/project/${ref}/storage/settings`,
|
||||
// items: [],
|
||||
// },
|
||||
{
|
||||
name: 'Billing & Usage',
|
||||
key: 'billing',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
IconEdit,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import Flag from 'components/ui/Flag/Flag'
|
||||
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
|
||||
import { STORAGE_ROW_STATUS } from 'components/to-be-cleaned/Storage/Storage.constants'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
@@ -24,7 +25,7 @@ interface Props {}
|
||||
const StorageMenu: FC<Props> = () => {
|
||||
const router = useRouter()
|
||||
const { ref, bucketId } = router.query
|
||||
const page = router.pathname.split('/')[4] as undefined | 'policies' | 'usage'
|
||||
const page = router.pathname.split('/')[4] as undefined | 'policies' | 'settings' | 'usage'
|
||||
|
||||
const { ui } = useStore()
|
||||
const projectRef = ui.selectedProject?.ref
|
||||
@@ -92,8 +93,15 @@ const StorageMenu: FC<Props> = () => {
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<Menu.Group title="Settings" />
|
||||
<Menu.Group title="Configuration" />
|
||||
|
||||
<Flag name="storageSettings">
|
||||
<Link href={`/project/${projectRef}/storage/settings`}>
|
||||
<Menu.Item rounded active={page === 'settings'}>
|
||||
<Typography.Text className="truncate">Settings</Typography.Text>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
</Flag>
|
||||
<Link href={`/project/${projectRef}/storage/policies`}>
|
||||
<Menu.Item rounded active={page === 'policies'}>
|
||||
<Typography.Text className="truncate">Policies</Typography.Text>
|
||||
|
||||
@@ -27,15 +27,17 @@ const TableEditorLayout: FC<Props> = ({
|
||||
onDuplicateTable = () => {},
|
||||
children,
|
||||
}) => {
|
||||
const { meta } = useStore()
|
||||
const { meta, ui } = useStore()
|
||||
const { isInitialized, isLoading, error } = meta.tables
|
||||
|
||||
const [loaded, setLoaded] = useState<boolean>(isInitialized)
|
||||
|
||||
useEffect(() => {
|
||||
meta.schemas.load()
|
||||
meta.tables.load()
|
||||
}, [])
|
||||
if (ui.selectedProject) {
|
||||
meta.schemas.load()
|
||||
meta.tables.load()
|
||||
}
|
||||
}, [ui.selectedProject])
|
||||
|
||||
useEffect(() => {
|
||||
let cancel = false
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import BaseLayout from 'components/layouts'
|
||||
import { IconChevronRight, Typography } from '@supabase/ui'
|
||||
import { IconChevronRight } from '@supabase/ui'
|
||||
|
||||
const WizardLayout: FC<any> = ({ organization, project, children }) => {
|
||||
return (
|
||||
<BaseLayout hideHeader hideIconBar>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<Header organization={organization} project={project} />
|
||||
<div className="overflow-auto">
|
||||
<section className="mx-auto max-w-2xl relative my-10 has-slide-in slide-in">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<Header organization={organization} project={project} />
|
||||
<div className="overflow-auto">
|
||||
<section className="mx-auto max-w-2xl relative my-10 has-slide-in slide-in">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default WizardLayout
|
||||
|
||||
@@ -71,18 +71,16 @@ export const DisplayApiSettings = () => {
|
||||
label={
|
||||
<>
|
||||
{x.tags?.split(',').map((x: any, i: number) => (
|
||||
<code key={`${x}${i}`} className="text-xs text-white px-2">
|
||||
<code key={`${x}${i}`} className="text-xs text-code">
|
||||
{x}
|
||||
</code>
|
||||
))}
|
||||
{x.tags === 'service_role' && (
|
||||
<>
|
||||
<code className="text-xs bg-red-900 px-2 ml-1 text-white">{'secret'}</code>
|
||||
<code className="text-xs bg-red-900 text-white">{'secret'}</code>
|
||||
</>
|
||||
)}
|
||||
{x.tags === 'anon' && (
|
||||
<code className="text-xs text-white px-2 ml-1">{'public'}</code>
|
||||
)}
|
||||
{x.tags === 'anon' && <code className="text-xs text-code">{'public'}</code>}
|
||||
</>
|
||||
}
|
||||
readOnly
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { toJS } from 'mobx'
|
||||
import { Button, Divider, Dropdown, IconPlus } from '@supabase/ui'
|
||||
import { Button, Dropdown, IconPlus } from '@supabase/ui'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
const OrganizationDropdown = ({ organizations }) => {
|
||||
const router = useRouter()
|
||||
@@ -39,4 +40,4 @@ const OrganizationDropdown = ({ organizations }) => {
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
export default OrganizationDropdown
|
||||
export default observer(OrganizationDropdown)
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Modal } from '@supabase/ui'
|
||||
import { Button, Modal } from '@supabase/ui'
|
||||
import { render, unmountComponentAtNode } from 'react-dom'
|
||||
|
||||
function ConfirmModal({
|
||||
@@ -33,12 +33,30 @@ function ConfirmModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header={title}
|
||||
variant={variant}
|
||||
visible={true}
|
||||
size="small"
|
||||
onCancel={onCancelClick}
|
||||
onConfirm={onConfirmClick}
|
||||
onConfirm={onCancelClick}
|
||||
loading={loading}
|
||||
customFooter={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="default" onClick={() => onCancelClick()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => onConfirmClick()} loading={loading}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
children={
|
||||
description && (
|
||||
<Modal.Content>
|
||||
<p className="text-sm text-scale-1100 py-4">{description}</p>
|
||||
</Modal.Content>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import useSWR from 'swr'
|
||||
import { FC, useState } from 'react'
|
||||
import { AutoField } from 'uniforms-bootstrap4'
|
||||
import { Typography } from '@supabase/ui'
|
||||
|
||||
import { API_URL, STORAGE_FILE_SIZE_LIMIT_MAX_BYTES } from 'lib/constants'
|
||||
import { patch, get } from 'lib/common/fetch'
|
||||
import { useStore } from 'hooks'
|
||||
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
|
||||
const StorageSettings: FC<any> = ({ projectRef }) => {
|
||||
const { data, error } = useSWR(`${API_URL}/projects/${projectRef}/config?app=storage`, get)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6 mx-auto sm:w-full md:w-3/4 text-center">
|
||||
<Typography.Title level={3}>Error loading storage settings</Typography.Title>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="p-6 mx-auto sm:w-full md:w-3/4 text-center">
|
||||
<Typography.Title level={3}>Loading...</Typography.Title>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <StorageConfig config={data} projectRef={projectRef} />
|
||||
}
|
||||
|
||||
const StorageConfig = ({ config, projectRef }: any) => {
|
||||
const { ui } = useStore()
|
||||
|
||||
const { isFreeTier } = config
|
||||
const [updates, setUpdates] = useState(config)
|
||||
|
||||
const updateConfig = async (updatedConfig: any) => {
|
||||
try {
|
||||
const response = await patch(
|
||||
`${API_URL}/projects/${projectRef}/config?app=storage`,
|
||||
updatedConfig
|
||||
)
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
} else {
|
||||
ui.setNotification({ category: 'success', message: 'Settings saved' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
ui.setNotification({ category: 'error', message: `Update config failed: ${error.message}` })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full min-h-full">
|
||||
<SchemaFormPanel
|
||||
title="Settings"
|
||||
schema={{
|
||||
properties: {
|
||||
fileSizeLimit: {
|
||||
help: 'The maximum size in bytes of a file that can be uploaded.',
|
||||
maximum: STORAGE_FILE_SIZE_LIMIT_MAX_BYTES,
|
||||
minimum: 0,
|
||||
title: 'Upload file size limit',
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
required: ['fileSizeLimit'],
|
||||
type: 'object',
|
||||
}}
|
||||
model={updates}
|
||||
onSubmit={(model: any) => updateConfig(model)}
|
||||
onReset={() => setUpdates(config)}
|
||||
>
|
||||
<AutoField
|
||||
disabled={isFreeTier}
|
||||
name="fileSizeLimit"
|
||||
showInlineError
|
||||
errorMessage={`Must be a number up to ${new Intl.NumberFormat('en-US').format(
|
||||
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES
|
||||
)} bytes.`}
|
||||
/>
|
||||
{isFreeTier && (
|
||||
<UpgradeToPro
|
||||
primaryText="Free Plan has a fixed upload file size limit of 50 MB."
|
||||
projectRef={projectRef}
|
||||
secondaryText="Please upgrade to Pro plan for a configurable upload file size limit of up to 5 GB."
|
||||
/>
|
||||
)}
|
||||
</SchemaFormPanel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StorageSettings
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { STORAGE_ROW_STATUS } from './Storage.constants'
|
||||
import Flag from 'components/ui/Flag/Flag'
|
||||
|
||||
// [Joshen] I think this file is no longer in use, double check before removing (replaced by StorageMenu.tsx)
|
||||
|
||||
const BucketRow = ({
|
||||
bucket = {},
|
||||
@@ -137,8 +140,23 @@ const StorageSidebar = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-4 px-3">
|
||||
<Menu.Group title="Settings" />
|
||||
<Menu.Group title="Configuration" />
|
||||
<div className="space-y-1">
|
||||
<Flag name="storageSettings">
|
||||
<div>
|
||||
<Link href={`/project/${ref}/storage/settings`}>
|
||||
<a>
|
||||
<Menu.Item
|
||||
key="settings"
|
||||
rounded
|
||||
active={router.pathname === '/project/[ref]/storage/settings'}
|
||||
>
|
||||
<Typography.Text>Settings</Typography.Text>
|
||||
</Menu.Item>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</Flag>
|
||||
<div>
|
||||
<Link href={`/project/${ref}/storage/policies`}>
|
||||
<a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import StorageExplorer from './StorageExplorer/StorageExplorer'
|
||||
import StoragePolicies from './StoragePolicies/StoragePolicies'
|
||||
import StorageSettings from './StorageSettings/StorageSettings'
|
||||
import StorageUsage from './StorageUsage/StorageUsage'
|
||||
import StorageSidebar from './StorageSidebar'
|
||||
|
||||
@@ -10,6 +11,7 @@ import ToggleBucketPublicModal from './ToggleBucketPublicModal'
|
||||
export {
|
||||
StorageExplorer,
|
||||
StoragePolicies,
|
||||
StorageSettings,
|
||||
StorageUsage,
|
||||
StorageSidebar,
|
||||
CreateBucketModal,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Button, IconClock, Typography } from '@supabase/ui'
|
||||
import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props {
|
||||
primaryText: string
|
||||
projectRef: string
|
||||
secondaryText: string
|
||||
}
|
||||
|
||||
const UpgradeToPro: FC<Props> = ({ primaryText, projectRef, secondaryText }) => (
|
||||
<div
|
||||
className={[
|
||||
'block w-full py-4 px-6 border rounded border-opacity-20',
|
||||
'bg-gray-100 border-gray-600',
|
||||
'dark:bg-gray-400 dark:border-gray-300',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
<div className="mt-1">
|
||||
<IconClock size="large" />
|
||||
</div>
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div>
|
||||
<p>{primaryText}</p>
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">{secondaryText}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/project/${projectRef}/settings/billing`}>
|
||||
<Button type="primary">Upgrade to Pro</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default UpgradeToPro
|
||||
@@ -42,11 +42,14 @@ export function withAuth(
|
||||
if (!profile) {
|
||||
ui.setProfile(undefined)
|
||||
} else if (returning !== 'minimal') {
|
||||
const { organizations, ...userProfile } = profile
|
||||
const projects: Project[] = flatten(organizations?.map((org: any) => org.projects))
|
||||
app.organizations.initialDataArray(organizations)
|
||||
app.projects.initialDataArray(projects)
|
||||
ui.setProfile(userProfile)
|
||||
ui.setProfile(profile)
|
||||
|
||||
if (!app.organizations.isInitialized) {
|
||||
app.organizations.load()
|
||||
}
|
||||
if (!app.projects.isInitialized) {
|
||||
app.projects.load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,3 +132,5 @@ export const PASSWORD_STRENGTH_PERCENTAGE = {
|
||||
}
|
||||
|
||||
export const DEFAULT_FREE_PROJECTS_LIMIT = 2
|
||||
|
||||
export const STORAGE_FILE_SIZE_LIMIT_MAX_BYTES = 5 * 1024 * 1024 * 1024 // 5 GB
|
||||
Generated
+3
-3
@@ -2704,9 +2704,9 @@
|
||||
}
|
||||
},
|
||||
"@supabase/ui": {
|
||||
"version": "0.37.0-alpha.41",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.41.tgz",
|
||||
"integrity": "sha512-ie8FfK6tUtCAlGmITwNwzhv7GStfu6xGhGUbHDq3tk0HjLuiKKkVg7a2OEdAh4TcJIYRBVBN65XCTvbfSvAUhA==",
|
||||
"version": "0.37.0-alpha.43",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.43.tgz",
|
||||
"integrity": "sha512-sFPzzxc71S/E/HOLSho5g8+dkZ3MY2VW4RxFiG37U7YgHmb2ItYyfc4O2j1NNSkoLYHJkqxEtDVG1tjJQtdmkQ==",
|
||||
"requires": {
|
||||
"@headlessui/react": "^1.0.0",
|
||||
"@mertasan/tailwindcss-variables": "^2.0.1",
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
"@supabase/react-data-grid": "^7.1.0-beta.7",
|
||||
"@supabase/shared-types": "^0.1.15",
|
||||
"@supabase/supabase-js": "^1.29.1",
|
||||
"@supabase/ui": "0.37.0-alpha.41",
|
||||
"@supabase/ui": "0.37.0-alpha.43",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"clipboard": "^2.0.8",
|
||||
|
||||
+12
-18
@@ -2,10 +2,9 @@ import { NextPage } from 'next'
|
||||
import Image from 'next/image'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Button, Typography } from '@supabase/ui'
|
||||
import { Button } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
|
||||
const Error404: NextPage = ({}) => {
|
||||
const { ui } = useStore()
|
||||
@@ -28,7 +27,7 @@ const Error404: NextPage = ({}) => {
|
||||
<a href="/">
|
||||
<Image
|
||||
src={theme == 'dark' ? '/img/supabase-dark.svg' : '/img/supabase-light.svg'}
|
||||
alt=""
|
||||
alt="supabase"
|
||||
height={24}
|
||||
width={120}
|
||||
/>
|
||||
@@ -37,31 +36,26 @@ const Error404: NextPage = ({}) => {
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="absolute">
|
||||
<Typography.Title
|
||||
className={`select-none filter transition opacity-[5%] duration-200 ${
|
||||
show404 ? 'blur-sm' : 'blur-none'
|
||||
}`}
|
||||
style={{ fontSize: '28rem' }}
|
||||
>
|
||||
404
|
||||
</Typography.Title>
|
||||
<div
|
||||
className={`absolute select-none filter transition opacity-[5%] duration-200 ${
|
||||
show404 ? 'blur-sm' : 'blur-none'
|
||||
}`}
|
||||
>
|
||||
<h1 style={{ fontSize: '28rem' }}>404</h1>
|
||||
</div>
|
||||
<div
|
||||
className={`transition flex flex-col space-y-6 items-center justify-center ${
|
||||
show404 ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="w-[320px] flex flex-col items-center justify-center space-y-3">
|
||||
<Typography.Title level={3}>Looking for something? 🔍</Typography.Title>
|
||||
<Typography.Text className="text-center">
|
||||
We couldn't find the page that you're looking for!
|
||||
</Typography.Text>
|
||||
<div className="w-[380px] flex flex-col items-center justify-center space-y-3 text-center">
|
||||
<h3 className="text-xl">Looking for something? 🔍</h3>
|
||||
<p className="text-scale-1100">We couldn't find the page that you're looking for!</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Button>Head back</Button>
|
||||
<Button size="small">Head back</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+22
-12
@@ -7,15 +7,16 @@ import { Typography } from '@supabase/ui'
|
||||
|
||||
import { Project } 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 Connecting from 'components/ui/Loading'
|
||||
import { AccountLayout } from 'components/layouts'
|
||||
import { auth } from 'lib/gotrue'
|
||||
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/to-be-cleaned/ModalsDeprecated/TextConfirmModal'
|
||||
import Connecting from 'components/ui/Loading'
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { app, ui } = useStore()
|
||||
@@ -88,20 +89,29 @@ const Home: NextPage = () => {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="p-4">
|
||||
{IS_PLATFORM && (
|
||||
<div className="my-2">
|
||||
<div className="flex">
|
||||
<div className="">
|
||||
<OrganizationDropdown organizations={app.organizations} />
|
||||
{app.organizations.isLoading ? (
|
||||
<div className="flex h-full items-center justify-center space-x-2">
|
||||
<Connecting />
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4">
|
||||
{IS_PLATFORM && (
|
||||
<div className="my-2">
|
||||
<div className="flex">
|
||||
<div className="">
|
||||
<OrganizationDropdown organizations={app.organizations} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="my-8 space-y-8">
|
||||
<ProjectList
|
||||
onSelectDelete={onSelectDeleteProject}
|
||||
onSelectRestore={onRestoreProject}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="my-8 space-y-8">
|
||||
<ProjectList onSelectDelete={onSelectDeleteProject} onSelectRestore={onRestoreProject} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<TextConfirmModal
|
||||
visible={!isUndefined(selectedProjectToDelete)}
|
||||
title="Are you absolutely sure?"
|
||||
|
||||
+23
-19
@@ -79,7 +79,8 @@ export const Wizard = observer(() => {
|
||||
const totalFreeProjects = subscriptionStats.total_free_projects
|
||||
const freeProjectsLimit = ui.profile?.free_project_limit ?? DEFAULT_FREE_PROJECTS_LIMIT
|
||||
|
||||
const isEmptyOrganizations = organizations.length <= 0
|
||||
const isOrganizationOwner = currentOrg?.is_owner || !app.organizations.isInitialized
|
||||
const isEmptyOrganizations = organizations.length <= 0 && app.organizations.isInitialized
|
||||
const isEmptyPaymentMethod = stripeCustomer
|
||||
? stripeCustomer.paymentMethods?.data?.length <= 0
|
||||
: undefined
|
||||
@@ -190,6 +191,7 @@ export const Wizard = observer(() => {
|
||||
<WizardLayout organization={currentOrg} project={null}>
|
||||
<Panel
|
||||
hideHeaderStyling
|
||||
isLoading={app.organizations.isInitialized}
|
||||
title={
|
||||
<div key="panel-title">
|
||||
<h3>Create a new project</h3>
|
||||
@@ -224,25 +226,27 @@ export const Wizard = observer(() => {
|
||||
</Panel.Content>
|
||||
|
||||
<Panel.Content className="Form section-block--body has-inputs-centered border-b border-t border-panel-border-interior-light dark:border-panel-border-interior-dark space-y-4">
|
||||
<Listbox
|
||||
label="Organization"
|
||||
layout="horizontal"
|
||||
value={currentOrg?.slug}
|
||||
onChange={(slug) => router.push(`/new/${slug}`)}
|
||||
>
|
||||
{organizations.map((x: any) => (
|
||||
<Listbox.Option
|
||||
key={x.id}
|
||||
label={x.name}
|
||||
value={x.slug}
|
||||
addOnBefore={() => <IconUsers />}
|
||||
>
|
||||
{x.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
{organizations.length > 0 && (
|
||||
<Listbox
|
||||
label="Organization"
|
||||
layout="horizontal"
|
||||
value={currentOrg?.slug}
|
||||
onChange={(slug) => router.push(`/new/${slug}`)}
|
||||
>
|
||||
{organizations.map((x: any) => (
|
||||
<Listbox.Option
|
||||
key={x.id}
|
||||
label={x.name}
|
||||
value={x.slug}
|
||||
addOnBefore={() => <IconUsers />}
|
||||
>
|
||||
{x.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
)}
|
||||
|
||||
{!currentOrg?.is_owner && <NotOrganizationOwnerWarning />}
|
||||
{!isOrganizationOwner && <NotOrganizationOwnerWarning />}
|
||||
</Panel.Content>
|
||||
|
||||
{canCreateProject && (
|
||||
|
||||
@@ -91,6 +91,7 @@ const OrgSettings = () => {
|
||||
export default withAuth(observer(OrgSettings))
|
||||
|
||||
const PageLayout = observer(() => {
|
||||
const { ui } = useStore()
|
||||
const PageState: any = useContext(PageContext)
|
||||
|
||||
return (
|
||||
@@ -104,7 +105,7 @@ const PageLayout = observer(() => {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<OrganizationSettings />
|
||||
{ui.selectedOrganization && <OrganizationSettings />}
|
||||
</AccountLayout>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,64 +1,29 @@
|
||||
import useSWR from 'swr'
|
||||
import { NextPage } from 'next'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { Typography, IconLoader } from '@supabase/ui'
|
||||
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import BaseLayout from 'components/layouts'
|
||||
import {
|
||||
ExampleProject,
|
||||
ClientLibrary,
|
||||
ProjectUsage,
|
||||
NewProjectPanel,
|
||||
} from 'components/interfaces/Home'
|
||||
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 BaseLayout from 'components/layouts'
|
||||
import ProjectUsageSection from 'components/interfaces/Home/ProjectUsageSection'
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { ui } = useStore()
|
||||
|
||||
const project = ui.selectedProject
|
||||
const projectName =
|
||||
project?.ref !== 'default'
|
||||
? project?.name ?? 'Welcome to your project'
|
||||
project?.ref !== 'default' && project?.name !== undefined
|
||||
? project?.name
|
||||
: 'Welcome to your project'
|
||||
|
||||
const { data: usage, error: usageError }: any = useSWR(
|
||||
`${API_URL}/projects/${project?.ref}/usage`,
|
||||
get
|
||||
)
|
||||
|
||||
if (usageError) {
|
||||
return <Typography.Text type="danger">Error loading data {usageError.message}</Typography.Text>
|
||||
}
|
||||
|
||||
const hasProjectData =
|
||||
usage && (usage?.bucketSize || (usage?.authUsers ?? '0') !== '0' || usage?.dbTables)
|
||||
? true
|
||||
: false
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
<div className="max-w-7xl mx-auto w-full my-16 space-y-16">
|
||||
<div className="mx-6 flex space-x-6 items-center">
|
||||
<h1 className="text-3xl">{projectName}</h1>
|
||||
</div>
|
||||
{IS_PLATFORM && project && (
|
||||
<>
|
||||
{isUndefined(usage) ? (
|
||||
<div className="w-full flex justify-center items-center space-x-2">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<p className="text-sm">Retrieving project usage statistics</p>
|
||||
</div>
|
||||
) : !usage.error && hasProjectData ? (
|
||||
<ProjectUsage project={project} />
|
||||
) : (
|
||||
<NewProjectPanel />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{IS_PLATFORM && <ProjectUsageSection />}
|
||||
<div className="space-y-8">
|
||||
<div className="mx-6">
|
||||
<Typography.Title level={4}>Client libraries</Typography.Title>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useMonaco } from '@monaco-editor/react'
|
||||
|
||||
import {
|
||||
useSqlEditorStore,
|
||||
SqlEditorContext,
|
||||
@@ -13,28 +11,20 @@ import TabWelcome from 'components/to-be-cleaned/SqlEditor/TabWelcome'
|
||||
import TabSqlQuery from 'components/to-be-cleaned/SqlEditor/TabSqlQuery'
|
||||
import getPgsqlCompletionProvider from 'components/to-be-cleaned/SqlEditor/PgsqlCompletionProvider'
|
||||
import getPgsqlSignatureHelpProvider from 'components/to-be-cleaned/SqlEditor/PgsqlSignatureHelpProvider'
|
||||
|
||||
import { useProjectContentStore } from 'stores/projectContentStore'
|
||||
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { SQLEditorLayout } from 'components/layouts'
|
||||
import BaseLayout from 'components/layouts'
|
||||
|
||||
const PageConfig = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
|
||||
const { meta, ui } = useStore()
|
||||
const { profile: user } = ui
|
||||
|
||||
const contentStore: any = useProjectContentStore(ref)
|
||||
const sqlEditorStore: any = useSqlEditorStore(ref, meta)
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
* Load persited data
|
||||
*/
|
||||
loadPersistantData()
|
||||
}, [ref])
|
||||
if (ui.selectedProject !== undefined) {
|
||||
loadPersistantData()
|
||||
}
|
||||
}, [ui.selectedProject])
|
||||
|
||||
/*
|
||||
* Load persited data
|
||||
@@ -44,23 +34,24 @@ const PageConfig = () => {
|
||||
await sqlEditorStore.loadRemotePersistentData(contentStore, (user as any)?.id)
|
||||
}
|
||||
|
||||
if (ui.selectedProject === undefined) {
|
||||
return <BaseLayout children={undefined} />
|
||||
}
|
||||
|
||||
const contentStore: any = useProjectContentStore(ui.selectedProject.ref)
|
||||
const sqlEditorStore: any = useSqlEditorStore(ui.selectedProject.ref, meta)
|
||||
|
||||
return (
|
||||
<SqlEditorContext.Provider value={sqlEditorStore}>
|
||||
<PageLayout />
|
||||
<SQLEditorLayout title="SQL">
|
||||
<SqlEditor />
|
||||
</SQLEditorLayout>
|
||||
</SqlEditorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(PageConfig))
|
||||
|
||||
const PageLayout = observer(() => {
|
||||
return (
|
||||
<SQLEditorLayout title="SQL">
|
||||
<SqlEditor />
|
||||
</SQLEditorLayout>
|
||||
)
|
||||
})
|
||||
|
||||
const SqlEditor = observer(() => {
|
||||
const { meta, ui } = useStore()
|
||||
const { isDarkTheme } = ui
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { StorageLayout } from 'components/layouts'
|
||||
import { StorageSettings } from 'components/to-be-cleaned/Storage'
|
||||
|
||||
/**
|
||||
* PageLayout is used to setup layout - as usual it will requires inject global store
|
||||
*/
|
||||
const PageLayout = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
|
||||
const { ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
useEffect(() => {
|
||||
if (project && project.status === PROJECT_STATUS.INACTIVE) {
|
||||
post(`${API_URL}/projects/${ref}/restore`, {})
|
||||
}
|
||||
}, [project])
|
||||
|
||||
if (!project) return <div></div>
|
||||
|
||||
return (
|
||||
<StorageLayout title="Settings">
|
||||
<div className="storage-container flex flex-grow p-4">
|
||||
<StorageSettings projectRef={ref} />
|
||||
</div>
|
||||
</StorageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(PageLayout))
|
||||
@@ -5,9 +5,7 @@ import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { withAuth } from 'hooks'
|
||||
import BaseLayout from 'components/layouts'
|
||||
import ProjectList from 'components/interfaces/Home/ProjectList'
|
||||
import { Typography } from '@supabase/ui'
|
||||
|
||||
interface Props {}
|
||||
|
||||
@@ -50,7 +48,7 @@ const GenericProjectPage: NextPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayout hideHeader hideIconBar>
|
||||
<div>
|
||||
<Header />
|
||||
<div className="py-8 w-full max-w-5xl mx-auto">
|
||||
<h3 className="text-2xl">Select a project to continue</h3>
|
||||
@@ -62,7 +60,7 @@ const GenericProjectPage: NextPage = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { configure } from 'mobx'
|
||||
import { configure, reaction } from 'mobx'
|
||||
import AppStore, { IAppStore } from './app/AppStore'
|
||||
import MetaStore, { IMetaStore } from './pgmeta/MetaStore'
|
||||
import UiStore, { IUiStore } from './UiStore'
|
||||
@@ -32,17 +32,38 @@ export class RootStore implements IRootStore {
|
||||
connectionString: '',
|
||||
})
|
||||
this.app = new AppStore(this)
|
||||
|
||||
/**
|
||||
* TODO: meta and content are not observable
|
||||
* meaning that when meta and content object change mobx doesnt trigger new event
|
||||
*
|
||||
* Workaround for now
|
||||
* we need to use ui.selectedProject along with meta and content
|
||||
* cos whenever ui.selectedProject changes, the reaction will create new meta and content stores
|
||||
*/
|
||||
reaction(
|
||||
() => this.ui.selectedProject,
|
||||
(selectedProject) => {
|
||||
if (selectedProject) {
|
||||
this.content = new ProjectContentStore(this, { projectRef: selectedProject.ref })
|
||||
this.meta = new MetaStore(this, {
|
||||
projectRef: selectedProject.ref,
|
||||
connectionString: selectedProject.connectionString ?? '',
|
||||
})
|
||||
} else {
|
||||
this.content = new ProjectContentStore(this, { projectRef: '' })
|
||||
this.meta = new MetaStore(this, {
|
||||
projectRef: '',
|
||||
connectionString: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setProjectRef(value?: string) {
|
||||
if (this.ui.selectedProject?.ref != value) {
|
||||
this.ui.setProjectRef(value)
|
||||
this.content = new ProjectContentStore(this, { projectRef: value || '' })
|
||||
this.meta = new MetaStore(this, {
|
||||
projectRef: value || '',
|
||||
connectionString: this.ui.selectedProject?.connectionString ?? '',
|
||||
})
|
||||
}
|
||||
if (this.ui.selectedProject?.ref === value) return
|
||||
this.ui.setProjectRef(value)
|
||||
}
|
||||
|
||||
setOrganizationSlug(value?: string) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash'
|
||||
import { values } from 'mobx'
|
||||
import { Organization, Project } from 'types'
|
||||
|
||||
import { API_URL, STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { IRootStore } from '../RootStore'
|
||||
import DatabaseStore, { IDatabaseStore } from './DatabaseStore'
|
||||
import OrganizationStore from './OrganizationStore'
|
||||
|
||||
@@ -13,16 +13,4 @@ export default class ProjectStore extends PostgresMetaInterface<Project> {
|
||||
) {
|
||||
super(rootStore, dataUrl, headers, options)
|
||||
}
|
||||
|
||||
initialDataArray(value: Project[]) {
|
||||
const finalValue = value.map((project: any) => {
|
||||
const kpsVersion =
|
||||
project?.services?.length > 0
|
||||
? project?.services[0]?.infrastructure[0]?.app_versions?.version
|
||||
: undefined
|
||||
|
||||
return { ...project, kpsVersion }
|
||||
})
|
||||
super.initialDataArray(finalValue)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export default class PostgresMetaInterface<T> implements IPostgresMetaInterface<
|
||||
}
|
||||
|
||||
get isInitialized() {
|
||||
return this.state !== this.STATES.INITIAL
|
||||
return this.state === this.STATES.LOADED || this.state === this.STATES.ERROR
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
|
||||
@@ -38,7 +38,6 @@ export default class ProjectContentStore implements IProjectContentStore {
|
||||
|
||||
constructor(rootStore: IRootStore, options: { projectRef: string }) {
|
||||
const { projectRef } = options
|
||||
|
||||
this.rootStore = rootStore
|
||||
this.baseUrl = `${API_URL}/projects/${projectRef}/content`
|
||||
makeAutoObservable(this)
|
||||
|
||||
@@ -147,8 +147,11 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.input-mono input {
|
||||
@apply font-mono;
|
||||
.input-mono {
|
||||
input,
|
||||
textarea {
|
||||
@apply font-mono;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
|
||||
Reference in new issue
Block a user