diff --git a/studio/components/interfaces/App/RouteValidationWrapper.tsx b/studio/components/interfaces/App/RouteValidationWrapper.tsx index ac97fd6a19e..5dbc8420ccc 100644 --- a/studio/components/interfaces/App/RouteValidationWrapper.tsx +++ b/studio/components/interfaces/App/RouteValidationWrapper.tsx @@ -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} } diff --git a/studio/components/interfaces/Database/Backups/BackupsList.tsx b/studio/components/interfaces/Database/Backups/BackupsList.tsx index 8ba7c4151bc..a66e989f6bf 100644 --- a/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -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 = ({}) => { if (tierId === STRIPE_PRODUCT_IDS.FREE) { return ( -
-
-
- -
-
-
-

Free Plan does not include project backups.

-
-

- Please upgrade to Pro plan for up to 7 days of backups. -

-
-
- - - -
-
-
+ ) } diff --git a/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx b/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx index e3c627806f4..d7424d22556 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx @@ -27,21 +27,23 @@ const ProjectCard: FC = ({ - {name} +
+ {name} {paused && ( - -
- - Project paused -
-
+
+ +
+ + Project paused +
+
+
)}
} description={''} footer={ -
+
{desc} {paused && (
diff --git a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx index 540e544fa99..ca96483e6e7 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx @@ -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 = ({ 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 = ({ const isEmpty = sortedProjects?.length == 0 return (
- -

{name}

+

{name}

+ +
    + {!isLoadingProjects && isEmpty && ( +
    +
    +

    No projects

    +

    + Get started by creating a new project. +

    +
    +
    + +
    +
    + )} + {sortedProjects?.map((project: Project) => ( + onSelectDelete(project)} + onSelectRestore={() => onSelectRestore(project)} + /> + ))} +
-
    - {!isLoading && isEmpty && ( -
    -
    -

    No projects

    -

    - Get started by creating a new project. -

    -
    -
    - -
    -
    - )} - {sortedProjects?.map((project: Project) => ( - onSelectDelete(project)} - onSelectRestore={() => onSelectRestore(project)} - /> - ))} -
) })} diff --git a/studio/components/interfaces/Home/ProjectUsageSection.tsx b/studio/components/interfaces/Home/ProjectUsageSection.tsx new file mode 100644 index 00000000000..4561dd7c3be --- /dev/null +++ b/studio/components/interfaces/Home/ProjectUsageSection.tsx @@ -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 Error loading data {usageError.message} + } + + return ( + <> + {usage === undefined ? ( +
+ +

Retrieving project usage statistics

+
+ ) : !usage.error && hasProjectData ? ( + + ) : ( + + )} + + ) +}) +export default ProjectUsageSection diff --git a/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts b/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts index 9146dd0e829..ba4808e166d 100644 --- a/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts +++ b/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts @@ -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', diff --git a/studio/components/interfaces/Settings/Logs/LogSelection.tsx b/studio/components/interfaces/Settings/Logs/LogSelection.tsx index 49f119f46dd..f4a37c23abf 100644 --- a/studio/components/interfaces/Settings/Logs/LogSelection.tsx +++ b/studio/components/interfaces/Settings/Logs/LogSelection.tsx @@ -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 = ({ log, onClose }) => {
- Event message - - {log.event_message}{' '} - +

Event message

+
- Metadata +

Metadata

diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx index 9b002618665..fbc150d3ab0 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx @@ -86,7 +86,7 @@ const Column: FC = ({ } rounded-md`} actions={
)} -
+
diff --git a/studio/components/layouts/AuthLayout/AuthLayout.tsx b/studio/components/layouts/AuthLayout/AuthLayout.tsx index 3feb7ba12a2..da038f2bfa5 100644 --- a/studio/components/layouts/AuthLayout/AuthLayout.tsx +++ b/studio/components/layouts/AuthLayout/AuthLayout.tsx @@ -24,8 +24,10 @@ const AuthLayout: FC = ({ title, children }) => { const [loaded, setLoaded] = useState(isInitialized) useEffect(() => { - meta.tables.load() - }, []) + if (ui.selectedProject) { + meta.tables.load() + } + }, [ui.selectedProject]) useEffect(() => { if (!isLoading && !loaded) { diff --git a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index 50f018fc642..5df6936a062 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -24,15 +24,17 @@ const DatabaseLayout: FC = ({ title, children }) => { const [loaded, setLoaded] = useState(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) { diff --git a/studio/components/layouts/DocsLayout/DocsLayout.tsx b/studio/components/layouts/DocsLayout/DocsLayout.tsx index ab09e77ad7c..14a85bd5dc9 100644 --- a/studio/components/layouts/DocsLayout/DocsLayout.tsx +++ b/studio/components/layouts/DocsLayout/DocsLayout.tsx @@ -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 ( diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx index 650c4d938cf..0dbd3a022e0 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx @@ -34,7 +34,8 @@ const FeedbackDropdown = () => { type="default" icon={} > - Feedback on this page? + Feedback + Feedback on this page? ) diff --git a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index d232be0baf3..be75878a99f 100644 --- a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -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 = ({ 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 ? ( - - {isLoading ? ( - - ) : ( -
{children}
- )} -
- ) : ( - <>{children} - ) - } else { - return <>{children} - } - } - return ( <> @@ -72,16 +41,16 @@ const ProjectLayout: FC = ({ {!hideIconBar && } {/* Product menu bar */} - {productMenu && !isLoading && ( + {productMenu} - )} +
{!hideHeader && } - {renderContent()} + {children}
@@ -89,3 +58,38 @@ const ProjectLayout: FC = ({ } export default observer(ProjectLayout) + +interface MenuBarWrapperProps { + isLoading: boolean + productMenu?: ReactNode +} + +const MenuBarWrapper: FC = observer(({ isLoading, productMenu, children }) => { + const { ui } = useStore() + return <>{!isLoading && productMenu && ui.selectedProject !== undefined ? children : null} +}) + +interface ContentWrapperProps { + isLoading: boolean +} + +const ContentWrapper: FC = 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 ? ( + + ) : requiresDbConnection ? ( + +
{children}
+
+ ) : ( + <>{children} + )} + + ) +}) diff --git a/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx b/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx index 9de4c3bdbef..2b0bbb61456 100644 --- a/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx +++ b/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx @@ -14,31 +14,18 @@ interface Props { const SQLEditorLayout: FC = ({ 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 ( - - - - ) - } - - return ( + return content.error ? ( + + + + ) : ( } diff --git a/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts b/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts index 1c6f13d71b2..dfcb29c0ab2 100644 --- a/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts +++ b/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts @@ -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', diff --git a/studio/components/layouts/StorageLayout/StorageMenu.tsx b/studio/components/layouts/StorageLayout/StorageMenu.tsx index 83e406a7d83..2cef4c5c904 100644 --- a/studio/components/layouts/StorageLayout/StorageMenu.tsx +++ b/studio/components/layouts/StorageLayout/StorageMenu.tsx @@ -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 = () => { 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 = () => {
- + + + + + Settings + + + Policies diff --git a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index 3ea80225b6c..787635589bd 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -27,15 +27,17 @@ const TableEditorLayout: FC = ({ onDuplicateTable = () => {}, children, }) => { - const { meta } = useStore() + const { meta, ui } = useStore() const { isInitialized, isLoading, error } = meta.tables const [loaded, setLoaded] = useState(isInitialized) useEffect(() => { - meta.schemas.load() - meta.tables.load() - }, []) + if (ui.selectedProject) { + meta.schemas.load() + meta.tables.load() + } + }, [ui.selectedProject]) useEffect(() => { let cancel = false diff --git a/studio/components/layouts/WizardLayout.tsx b/studio/components/layouts/WizardLayout.tsx index 2a526e3a8f4..e6ef990668e 100644 --- a/studio/components/layouts/WizardLayout.tsx +++ b/studio/components/layouts/WizardLayout.tsx @@ -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 = ({ organization, project, children }) => { return ( - -
-
-
-
- {children} -
-
+
+
+
+
+ {children} +
- +
) } export default WizardLayout diff --git a/studio/components/to-be-cleaned/DisplayProjectSettings.tsx b/studio/components/to-be-cleaned/DisplayProjectSettings.tsx index 6ab1f91e4a4..a120dc92352 100644 --- a/studio/components/to-be-cleaned/DisplayProjectSettings.tsx +++ b/studio/components/to-be-cleaned/DisplayProjectSettings.tsx @@ -71,18 +71,16 @@ export const DisplayApiSettings = () => { label={ <> {x.tags?.split(',').map((x: any, i: number) => ( - + {x} ))} {x.tags === 'service_role' && ( <> - {'secret'} + {'secret'} )} - {x.tags === 'anon' && ( - {'public'} - )} + {x.tags === 'anon' && {'public'}} } readOnly diff --git a/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js b/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js index 48652fc7519..97f93d6cba9 100644 --- a/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js +++ b/studio/components/to-be-cleaned/Dropdown/OrganizationDropdown.js @@ -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 }) => { ) } -export default OrganizationDropdown +export default observer(OrganizationDropdown) diff --git a/studio/components/to-be-cleaned/ModalsDeprecated/ConfirmModal.js b/studio/components/to-be-cleaned/ModalsDeprecated/ConfirmModal.js index 37a908a55d5..0b3ea97dcf8 100644 --- a/studio/components/to-be-cleaned/ModalsDeprecated/ConfirmModal.js +++ b/studio/components/to-be-cleaned/ModalsDeprecated/ConfirmModal.js @@ -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 ( + + +
+ } + children={ + description && ( + +

{description}

+
+ ) + } /> ) } diff --git a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx new file mode 100644 index 00000000000..657ca386959 --- /dev/null +++ b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx @@ -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 = ({ projectRef }) => { + const { data, error } = useSWR(`${API_URL}/projects/${projectRef}/config?app=storage`, get) + + if (error) { + return ( +
+ Error loading storage settings +
+ ) + } + + if (!data) { + return ( +
+ Loading... +
+ ) + } + + return +} + +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 ( +
+ updateConfig(model)} + onReset={() => setUpdates(config)} + > + + {isFreeTier && ( + + )} + +
+ ) +} + +export default StorageSettings diff --git a/studio/components/to-be-cleaned/Storage/StorageSidebar.js b/studio/components/to-be-cleaned/Storage/StorageSidebar.js index dc387937def..61378a165af 100644 --- a/studio/components/to-be-cleaned/Storage/StorageSidebar.js +++ b/studio/components/to-be-cleaned/Storage/StorageSidebar.js @@ -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 = ({
- +
+ + +
diff --git a/studio/components/to-be-cleaned/Storage/index.js b/studio/components/to-be-cleaned/Storage/index.js index f35c92af17f..e903df9cd68 100644 --- a/studio/components/to-be-cleaned/Storage/index.js +++ b/studio/components/to-be-cleaned/Storage/index.js @@ -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, diff --git a/studio/components/ui/UpgradeToPro.tsx b/studio/components/ui/UpgradeToPro.tsx new file mode 100644 index 00000000000..46bc5c0224b --- /dev/null +++ b/studio/components/ui/UpgradeToPro.tsx @@ -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 = ({ primaryText, projectRef, secondaryText }) => ( +
+
+
+ +
+
+
+

{primaryText}

+
+

{secondaryText}

+
+
+ + + +
+
+
+) + +export default UpgradeToPro diff --git a/studio/hooks/misc/withAuth.tsx b/studio/hooks/misc/withAuth.tsx index 680c4c34cb1..1ebbe3ef816 100644 --- a/studio/hooks/misc/withAuth.tsx +++ b/studio/hooks/misc/withAuth.tsx @@ -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() + } } } diff --git a/studio/lib/constants/infrastructure.ts b/studio/lib/constants/infrastructure.ts index 36e626507ce..d15adeb97fe 100644 --- a/studio/lib/constants/infrastructure.ts +++ b/studio/lib/constants/infrastructure.ts @@ -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 diff --git a/studio/package-lock.json b/studio/package-lock.json index 6127a589915..3ccdea4d60b 100644 --- a/studio/package-lock.json +++ b/studio/package-lock.json @@ -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", diff --git a/studio/package.json b/studio/package.json index df832fb91ae..da18b86e1e7 100644 --- a/studio/package.json +++ b/studio/package.json @@ -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", diff --git a/studio/pages/404.tsx b/studio/pages/404.tsx index 08f81121f4a..4c55a1f4f18 100644 --- a/studio/pages/404.tsx +++ b/studio/pages/404.tsx @@ -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 = ({}) => {
@@ -37,31 +36,26 @@ const Error404: NextPage = ({}) => {
-
- - 404 - +
+

404

-
- Looking for something? 🔍 - - We couldn't find the page that you're looking for! - +
+

Looking for something? 🔍

+

We couldn't find the page that you're looking for!

diff --git a/studio/pages/index.tsx b/studio/pages/index.tsx index 82813127134..46ddf306367 100644 --- a/studio/pages/index.tsx +++ b/studio/pages/index.tsx @@ -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 = () => { }, ]} > -
- {IS_PLATFORM && ( -
-
-
- + {app.organizations.isLoading ? ( +
+ +
+ ) : ( +
+ {IS_PLATFORM && ( +
+
+
+ +
+ )} +
+
- )} -
-
-
+ )} { 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(() => {

Create a new project

@@ -224,25 +226,27 @@ export const Wizard = observer(() => { - router.push(`/new/${slug}`)} - > - {organizations.map((x: any) => ( - } - > - {x.name} - - ))} - + {organizations.length > 0 && ( + router.push(`/new/${slug}`)} + > + {organizations.map((x: any) => ( + } + > + {x.name} + + ))} + + )} - {!currentOrg?.is_owner && } + {!isOrganizationOwner && } {canCreateProject && ( diff --git a/studio/pages/org/[slug]/settings.tsx b/studio/pages/org/[slug]/settings.tsx index 7161402b831..118d29ddfec 100644 --- a/studio/pages/org/[slug]/settings.tsx +++ b/studio/pages/org/[slug]/settings.tsx @@ -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(() => { }, ]} > - + {ui.selectedOrganization && } ) }) diff --git a/studio/pages/project/[ref]/index.tsx b/studio/pages/project/[ref]/index.tsx index 076d15751ff..36d861d5660 100644 --- a/studio/pages/project/[ref]/index.tsx +++ b/studio/pages/project/[ref]/index.tsx @@ -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 Error loading data {usageError.message} - } - - const hasProjectData = - usage && (usage?.bucketSize || (usage?.authUsers ?? '0') !== '0' || usage?.dbTables) - ? true - : false - return (

{projectName}

- {IS_PLATFORM && project && ( - <> - {isUndefined(usage) ? ( -
- -

Retrieving project usage statistics

-
- ) : !usage.error && hasProjectData ? ( - - ) : ( - - )} - - )} + {IS_PLATFORM && }
Client libraries diff --git a/studio/pages/project/[ref]/sql/index.tsx b/studio/pages/project/[ref]/sql/index.tsx index 6b48869468c..21e61263935 100644 --- a/studio/pages/project/[ref]/sql/index.tsx +++ b/studio/pages/project/[ref]/sql/index.tsx @@ -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 + } + + const contentStore: any = useProjectContentStore(ui.selectedProject.ref) + const sqlEditorStore: any = useSqlEditorStore(ui.selectedProject.ref, meta) + return ( - + + + ) } export default withAuth(observer(PageConfig)) -const PageLayout = observer(() => { - return ( - - - - ) -}) - const SqlEditor = observer(() => { const { meta, ui } = useStore() const { isDarkTheme } = ui diff --git a/studio/pages/project/[ref]/storage/settings.tsx b/studio/pages/project/[ref]/storage/settings.tsx new file mode 100644 index 00000000000..ecb47812553 --- /dev/null +++ b/studio/pages/project/[ref]/storage/settings.tsx @@ -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
+ + return ( + +
+ +
+
+ ) +} + +export default withAuth(observer(PageLayout)) diff --git a/studio/pages/project/_/[[...routeSlug]].tsx b/studio/pages/project/_/[[...routeSlug]].tsx index 73500eee0fc..b18f770153a 100644 --- a/studio/pages/project/_/[[...routeSlug]].tsx +++ b/studio/pages/project/_/[[...routeSlug]].tsx @@ -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 ( - +

Select a project to continue

@@ -62,7 +60,7 @@ const GenericProjectPage: NextPage = () => { />
-
+
) } diff --git a/studio/stores/RootStore.ts b/studio/stores/RootStore.ts index 4bd4eb7bf5c..57cc645b3af 100644 --- a/studio/stores/RootStore.ts +++ b/studio/stores/RootStore.ts @@ -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) { diff --git a/studio/stores/app/AppStore.ts b/studio/stores/app/AppStore.ts index 235cdda2682..4146516ddc2 100644 --- a/studio/stores/app/AppStore.ts +++ b/studio/stores/app/AppStore.ts @@ -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' diff --git a/studio/stores/app/ProjectStore.ts b/studio/stores/app/ProjectStore.ts index 80dfd492a80..0168c004e66 100644 --- a/studio/stores/app/ProjectStore.ts +++ b/studio/stores/app/ProjectStore.ts @@ -13,16 +13,4 @@ export default class ProjectStore extends PostgresMetaInterface { ) { 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) - } } diff --git a/studio/stores/common/PostgresMetaInterface.ts b/studio/stores/common/PostgresMetaInterface.ts index 3314521b081..343824ad984 100644 --- a/studio/stores/common/PostgresMetaInterface.ts +++ b/studio/stores/common/PostgresMetaInterface.ts @@ -87,7 +87,7 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< } get isInitialized() { - return this.state !== this.STATES.INITIAL + return this.state === this.STATES.LOADED || this.state === this.STATES.ERROR } async fetchData() { diff --git a/studio/stores/content/ProjectContentStore.ts b/studio/stores/content/ProjectContentStore.ts index 2952aaa42a0..4dd3e2c00ea 100644 --- a/studio/stores/content/ProjectContentStore.ts +++ b/studio/stores/content/ProjectContentStore.ts @@ -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) diff --git a/studio/styles/main.scss b/studio/styles/main.scss index cfe8c1eec35..61c8b616732 100644 --- a/studio/styles/main.scss +++ b/studio/styles/main.scss @@ -147,8 +147,11 @@ a { text-decoration: none; } -.input-mono input { - @apply font-mono; +.input-mono { + input, + textarea { + @apply font-mono; + } } input[type='radio'] {