Merge pull request #5777 from supabase/master

Prod deploy to studio
This commit is contained in:
Joshen Lim authored and GitHub committed 2022-03-03 14:05:38 +08:00
commit 95bce295b4
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 (
@@ -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
+8 -11
View File
@@ -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,
+38
View File
@@ -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
+8 -5
View File
@@ -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()
}
}
}
+2
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 && (
+2 -1
View File
@@ -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>
)
})
+8 -43
View File
@@ -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>
+15 -24
View File
@@ -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))
+2 -4
View File
@@ -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>
)
}
+30 -9
View File
@@ -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) {
+1 -1
View File
@@ -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'
-12
View File
@@ -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)
+5 -2
View File
@@ -147,8 +147,11 @@ a {
text-decoration: none;
}
.input-mono input {
@apply font-mono;
.input-mono {
input,
textarea {
@apply font-mono;
}
}
input[type='radio'] {