diff --git a/about/src/css/custom.css b/about/src/css/custom.css index dd3295ec8b0..7d12b14f42d 100755 --- a/about/src/css/custom.css +++ b/about/src/css/custom.css @@ -1032,6 +1032,7 @@ h4.method-list-item-label { h4.method-list-item-label .method-list-item-label-name { font-family: var(--ifm-font-family-monospace); font-size: 0.8rem; + margin-right: 4px; padding: var(--ifm-code-padding-vertical) 0; } h4.method-list-item-label .method-list-item-label-badge { diff --git a/apps/temp-docs/lib/docs.ts b/apps/temp-docs/lib/docs.ts index 3af7d930c39..05c2fa79f4c 100644 --- a/apps/temp-docs/lib/docs.ts +++ b/apps/temp-docs/lib/docs.ts @@ -32,14 +32,14 @@ function walk(dir: string) { const list = fs.readdirSync(dir) list.forEach(function (file) { file = dir + '/' + file - const stat = fs.statSync(file) - if (stat && stat.isDirectory()) { - /* Recurse into a subdirectory */ - results = results.concat(walk(file)) - } else { - /* Is a file */ - results.push(file) - } + let slugs = [] + + fs.readdirSync(dir).forEach((file) => { + let absolute = join(dir, file) + if (fs.statSync(absolute).isDirectory()) { + fs.readdirSync(absolute).forEach((subFile) => slugs.push(file.concat('/' + subFile))) + } + }) }) return results } diff --git a/apps/www/components/PricingTableRow.tsx b/apps/www/components/PricingTableRow.tsx index 6a611bceea0..91ca9bae8dd 100644 --- a/apps/www/components/PricingTableRow.tsx +++ b/apps/www/components/PricingTableRow.tsx @@ -28,7 +28,7 @@ const IncludedCheck = (props: any) => ( const Minus = (props: any) => ( <> { return (
-
{feat.title} + {feat.title} {feat.tooltip && ( - + )} {Object.values(feat.tiers).map((tier: any, i) => { return ( - + {typeof tier === 'boolean' && tier === true ? ( ) : typeof tier === 'boolean' && tier === false ? ( - +
+ +
) : ( {tier} )} @@ -113,7 +115,7 @@ export const PricingTableRowMobile = (props: any) => { return ( <> - + {category.features.map((feat: any, i: number) => { return ( - - + -
+
{category.icon ? : null} {category.title} @@ -129,16 +131,16 @@ export const PricingTableRowMobile = (props: any) => { -
+

{feat.title}

+ {typeof feat.tiers[tier] === 'boolean' && feat.tiers[tier] === true ? (
diff --git a/apps/www/data/Pricing.json b/apps/www/data/Pricing.json index ead301d5e44..5b4fb29578e 100644 --- a/apps/www/data/Pricing.json +++ b/apps/www/data/Pricing.json @@ -45,9 +45,17 @@ "enterprise": "30 days Point in Time backup" } }, + { + "title": "Point in time recovery", + "tiers": { + "free": false, + "pro": false, + "enterprise": true + } + }, { "title": "Pausing", - "tooltip": "Unused projects that have no activity or API requests will be shut down. They can be reactivated by logging into the dashboard", + "tooltip": "Projects that have no activity or API requests will be paused. They can be reactivated via the dashboard", "tiers": { "free": "Paused after 1 week inactivity", "pro": "Never", @@ -55,11 +63,11 @@ } }, { - "title": "Transfer limits", + "title": "Database egress", "tooltip": "Good news, only download operations are counted towards the limit", "tiers": { "free": "Up to 2GB", - "pro": "50GB included, then $0.09 per GB", + "pro": "50GB included per month, then $0.09 per GB", "enterprise": "Unlimited" } } @@ -78,6 +86,15 @@ "enterprise": "Unlimited" } }, + { + "title": "MAUs", + "tooltip": "Monthly Active Users; a user that has made an API request in the last month.", + "tiers": { + "free": "Up to 50,000 MAUs", + "pro": "100,000 included, then $0.00325 per MAU", + "enterprise": "1,000,000 included, then $0.00325 per MAU" + } + }, { "title": "Social OAuth providers", "tiers": { @@ -150,7 +167,7 @@ } }, { - "title": "Transfer limits", + "title": "Storage egress", "tooltip": "Good news, only download operations are counted towards the limit", "tiers": { "free": "Up to 2 GB", @@ -168,6 +185,7 @@ }, { "title": "Max file upload size", + "tooltip": "You can change the upload size in the dashboard", "tiers": { "free": "Up to 50MB", "pro": "Up to 5GB", @@ -183,16 +201,16 @@ { "title": "Runtime hours", "tiers": { - "free": "100 hours", - "pro": "1,000 hours", + "free": "100 hours per month", + "pro": "1,000 hours per month", "enterprise": "Unlimited" } }, { "title": "Invocations", "tiers": { - "free": "500K", - "pro": "2 Million", + "free": "500K per month", + "pro": "2 Million per month", "enterprise": "Unlimited" } }, @@ -308,7 +326,7 @@ "title": "Metrics endpoint", "tiers": { "free": false, - "pro": false, + "pro": true, "enterprise": true } }, @@ -343,6 +361,22 @@ "pro": false, "enterprise": true } + }, + { + "title": "Designated customer success engineer ", + "tiers": { + "free": false, + "pro": false, + "enterprise": true + } + }, + { + "title": "Security Questionnaire Help", + "tiers": { + "free": false, + "pro": false, + "enterprise": true + } } ] } diff --git a/apps/www/data/PricingAddOn.json b/apps/www/data/PricingAddOn.json index 0eaacfa10de..3de96ea0e20 100644 --- a/apps/www/data/PricingAddOn.json +++ b/apps/www/data/PricingAddOn.json @@ -37,12 +37,12 @@ { "key": "directConnections", "title": "Connections: Direct", - "value": "10 (recommended)" + "value": "30 (recommended)" }, { "key": "poolerConnections", "title": "Connections: Pooler", - "value": "50 (recommended)" + "value": "75 (recommended)" } ] }, @@ -61,7 +61,7 @@ { "key": "poolerConnections", "title": "Connections: Pooler", - "value": "500 (recommended)" + "value": "150 (recommended)" } ] }, @@ -80,7 +80,7 @@ { "key": "poolerConnections", "title": "Connections: Pooler", - "value": "1000 (recommended)" + "value": "300 (recommended)" } ] }, @@ -99,7 +99,7 @@ { "key": "poolerConnections", "title": "Connections: Pooler", - "value": "2000 (recommended)" + "value": "600 (recommended)" } ] }, @@ -113,12 +113,88 @@ { "key": "directConnections", "title": "Connections: Direct", - "value": "400 (recommended)" + "value": "350 (recommended)" }, { "key": "poolerConnections", "title": "Connections: Pooler", - "value": "4000 (recommended)" + "value": "1200 (recommended)" + } + ] + }, + { + "columns": [ + { "key": "plan", "title": "Plan", "value": "4XL" }, + { "key": "pricing", "title": "Pricing", "value": "$950" }, + { "key": "cpu", "title": "CPU", "value": "16-core ARM (dedicated)" }, + { "key": "memory", "title": "Memory", "value": "64 GB" }, + { "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 4,750 Mbps" }, + { + "key": "directConnections", + "title": "Connections: Direct", + "value": "420 (recommended)" + }, + { + "key": "poolerConnections", + "title": "Connections: Pooler", + "value": "2800 (recommended)" + } + ] + }, + { + "columns": [ + { "key": "plan", "title": "Plan", "value": "8XL" }, + { "key": "pricing", "title": "Pricing", "value": "$1,860" }, + { "key": "cpu", "title": "CPU", "value": "32-core ARM (dedicated)" }, + { "key": "memory", "title": "Memory", "value": "128 GB" }, + { "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 9,000 Mbps" }, + { + "key": "directConnections", + "title": "Connections: Direct", + "value": "450 (recommended)" + }, + { + "key": "poolerConnections", + "title": "Connections: Pooler", + "value": "5600 (recommended)" + } + ] + }, + { + "columns": [ + { "key": "plan", "title": "Plan", "value": "12XL" }, + { "key": "pricing", "title": "Pricing", "value": "$2,790" }, + { "key": "cpu", "title": "CPU", "value": "48-core ARM (dedicated)" }, + { "key": "memory", "title": "Memory", "value": "192 GB" }, + { "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 13,500 Mbps" }, + { + "key": "directConnections", + "title": "Connections: Direct", + "value": "480 (recommended)" + }, + { + "key": "poolerConnections", + "title": "Connections: Pooler", + "value": "8600 (recommended)" + } + ] + }, + { + "columns": [ + { "key": "plan", "title": "Plan", "value": "16XL" }, + { "key": "pricing", "title": "Pricing", "value": "$3,720" }, + { "key": "cpu", "title": "CPU", "value": "64-core ARM (dedicated)" }, + { "key": "memory", "title": "Memory", "value": "256 GB" }, + { "key": "diskIO", "title": "Disk IO Bandwidth", "value": "Up to 19,000 Mbps" }, + { + "key": "directConnections", + "title": "Connections: Direct", + "value": "500 (recommended)" + }, + { + "key": "poolerConnections", + "title": "Connections: Pooler", + "value": "11,600 (recommended)" } ] }, diff --git a/apps/www/pages/blog/[year]/[month]/[day]/[slug].tsx b/apps/www/pages/blog/[year]/[month]/[day]/[slug].tsx index bd7272de4cd..6ba7f9a0099 100644 --- a/apps/www/pages/blog/[year]/[month]/[day]/[slug].tsx +++ b/apps/www/pages/blog/[year]/[month]/[day]/[slug].tsx @@ -29,6 +29,30 @@ const components = { return }, ImageGrid, + img: (props: any) => { + const classes = [ + 'next-image--dynamic-fill', + 'from-brand-500 to-brand-500', + 'rounded border bg-gradient-to-r via-blue-500', + ] + + return ( +
+ +
+ ) + }, } // plugins for next-mdx-remote diff --git a/apps/www/pages/database/Database.tsx b/apps/www/pages/database/Database.tsx index 91880ee9280..6e0470ddeac 100644 --- a/apps/www/pages/database/Database.tsx +++ b/apps/www/pages/database/Database.tsx @@ -53,7 +53,7 @@ function Database() { openGraph={{ title: meta_title, description: meta_description, - url: `https://supabase.com/auth`, + url: `https://supabase.com/database`, images: [ { url: `https://supabase.com${basePath}/images/product/database/database-og.jpg`, diff --git a/apps/www/pages/nextjs/Nextjs.css b/apps/www/pages/nextjs/Nextjs.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/www/pages/nextjs/Nextjs.tsx b/apps/www/pages/nextjs/Nextjs.tsx new file mode 100644 index 00000000000..bd46c237240 --- /dev/null +++ b/apps/www/pages/nextjs/Nextjs.tsx @@ -0,0 +1,40 @@ +import { NextSeo } from 'next-seo' +import { useRouter } from 'next/router' +import { useEffect } from 'react' + +// NOTE: this is a placeholder for our future Next.js marketing page. For now it redirects to our Next.js guide. + +function Nextjs() { + // base path for images + const router = useRouter() + const { basePath } = router + + useEffect(() => { + router.push('/docs/guides/with-nextjs') + }, []) + + const meta_title = 'Next.js Database with Auth, Realtime, Fiel Storage, and more.' + const meta_description = + 'Supabase is the fastest way to build performant Next.js apps with Authentication & User Management, Realtime, File Storage on top of PostgreSQL Databases.' + + return ( + <> + + + ) +} + +export default Nextjs diff --git a/apps/www/pages/nextjs/index.tsx b/apps/www/pages/nextjs/index.tsx new file mode 100644 index 00000000000..1f3c1850d7d --- /dev/null +++ b/apps/www/pages/nextjs/index.tsx @@ -0,0 +1,3 @@ +import Nextjs from './Nextjs' + +export default Nextjs diff --git a/apps/www/pages/pricing/CHANGELOG.txt b/apps/www/pages/pricing/CHANGELOG.txt index cb67010b3d2..e3f3c76d83d 100644 --- a/apps/www/pages/pricing/CHANGELOG.txt +++ b/apps/www/pages/pricing/CHANGELOG.txt @@ -1,5 +1,27 @@ Pricing Changelog +### 16 June 2022 +---- + +Auth +- New item “MAUs (Monthly Active Users)” +Security & Compliance +- New item: “Security questionnaire help” for enterprise +- New item - “Metrics Endpoint” for enterprise and pro tier users +Database Compute Add ons +- New item: “4XL” +- New item: “8XL” +- New item: “12XL” +- New item: “16XL” +Database +- New item: “Point in time recovery” for enterprise +Clarifications +- General - Clearer language that we charge on a per project per month +- Storage - Uploads for Pro, changed from 3GB → 5GB in title card +- Storage - file upload size - clarified that it can be changed in dashboard +- All products - “Transfer” renamed to “Egress” +- Database compute add-ons - Number of connections more accurate +- Storage - Free tier egress per month clarified ### 31 March 2022 ---- diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index 1b86f5a1252..b6ab6433929 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -30,6 +30,7 @@ export default function IndexPage() { const tiers = [ { name: 'Free', + costUnit: '/month per project', href: 'https://app.supabase.com/new/new-project', priceMonthly: 0, warning: 'Limit of 2 free projects', @@ -39,6 +40,7 @@ export default function IndexPage() { 'Up to 2GB bandwidth', 'Up to 50MB file uploads', 'Social OAuth providers', + '50,000 monthly active users', 'Up to 500K Edge Function invocations', '1-day log retention', 'Community support', @@ -49,16 +51,17 @@ export default function IndexPage() { }, { name: 'Pro', + costUnit: '/month per project', href: 'https://app.supabase.com/new/new-project', from: true, priceMonthly: 25, - warning: 'per project', description: 'For production applications with the option to scale.', features: [ '8GB database & 100GB file storage', '50GB bandwidth', - '3GB file uploads', + '5GB file uploads', 'Social OAuth providers', + '100,000 monthly active users', '2M Edge Function invocations', 'Daily backups', '7-day log retention', @@ -181,21 +184,16 @@ export default function IndexPage() { border " > -
-

- {tier.name} -

+
+
+

{tier.name}

+ {tier.warning && ( +
+ {tier.warning} +
+ )} +
+

{tier.description}

{tier.priceMonthly !== undefined ? ( -
- {tier.from && ( - From - )} -
- ${tier.priceMonthly} - /mo -
- {tier.warning && ( -
- {tier.warning} +
+
+
+ + {typeof tier.priceMonthly !== 'string' && '$'} + {tier.priceMonthly} + + + {tier.costUnit} +
+
+ {tier.from && ( + + + any additional usage + )}
) : ( Contact Us )}
-

{tier.description}

- {tier.preface &&

{tier.preface}

} + {tier.preface &&

{tier.preface}

} {/*

Included with plan:

*/}
    {tier.features.map((feature) => ( @@ -645,16 +646,22 @@ export default function IndexPage() {
    {/* @ts-ignore */} {pricingFaq.map((faq, i) => { return ( - - {faq.answer} - +
    + {faq.question}} + id={`faq--${i.toString()}`} + > + {faq.answer} + +
    ) })}
    diff --git a/apps/www/styles/index.css b/apps/www/styles/index.css index d770487d130..13b6b8b6e55 100644 --- a/apps/www/styles/index.css +++ b/apps/www/styles/index.css @@ -223,3 +223,22 @@ table { .sbui-typography a { @apply break-words; } + +/* +* sets the image in @Next/Image components +* to respect the height of the content +* +*/ +.next-image--dynamic-fill { + width: 100%; + grid-column: 1 / -1; +} +.next-image--dynamic-fill > span { + position: relative !important; +} +.next-image--dynamic-fill img { + object-fit: contain; + width: 100% !important; + position: relative !important; + height: unset !important; +} diff --git a/examples/todo-list/nextjs-todo-list/pages/index.js b/examples/todo-list/nextjs-todo-list/pages/index.js index 273d9e302c6..beb5413e63d 100644 --- a/examples/todo-list/nextjs-todo-list/pages/index.js +++ b/examples/todo-list/nextjs-todo-list/pages/index.js @@ -9,12 +9,14 @@ export default function IndexPage() {
    {!user ? (
    - +
    + +
    ) : (
    = ({ const operation = policyFormFields?.command ?? '' const definition = policyFormFields?.definition ?? '' const check = policyFormFields?.check ?? '' - const selectedRoles = policyFormFields?.roles ?? [] + + // Filter out default public role (public if no roles selected) + const selectedRoles = (policyFormFields?.roles ?? []).filter((role: string) => role !== 'public') return (
    diff --git a/studio/components/interfaces/Organization/BillingSettings/AWSMarketplaceSubscription.tsx b/studio/components/interfaces/Organization/BillingSettings/AWSMarketplaceSubscription.tsx deleted file mode 100644 index cf175daa039..00000000000 --- a/studio/components/interfaces/Organization/BillingSettings/AWSMarketplaceSubscription.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { FC } from 'react' -import { Button, Typography } from '@supabase/ui' - -interface Props { - organization: any -} - -const AWSMarketplaceSubscription: FC = ({ organization }) => { - return ( -
    - - This organization is subscribed via AWS Marketplace. The subscription expires on{' '} - {organization.aws_marketplace.Entitlements[0].ExpirationDate.slice(0, 10)}. - - - -
    - ) -} - -export default AWSMarketplaceSubscription diff --git a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx index d1b47ec790b..9a2430dab8a 100644 --- a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx @@ -4,7 +4,6 @@ import { useStore } from 'hooks' import { API_URL } from 'lib/constants' import { get } from 'lib/common/fetch' -import AWSMarketplaceSubscription from './AWSMarketplaceSubscription' import ProjectsSummary from './ProjectsSummary' import CreditBalance from './CreditBalance' import PaymentMethods from './PaymentMethods' @@ -94,33 +93,27 @@ const BillingSettings: FC = ({ organization, projects = [] }) => { return (
    - {organization.aws_marketplace ? ( - - ) : ( - <> -
    - - - getPaymentMethods()} - /> - setCustomer({ ...customer, address })} - /> - setTaxIds(ids)} - /> -
    - - )} +
    + + + getPaymentMethods()} + /> + setCustomer({ ...customer, address })} + /> + setTaxIds(ids)} + /> +
    ) } diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.ts b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.ts index 3de594635e8..4ffc35756c3 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.ts +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.ts @@ -5,9 +5,6 @@ import { ExtensionsUpgrade, Notification, NotificationName, - PostgresqlUpgradeData, - ProjectExceedingTierLimitData, - ProjectUpdateData, ServerUpgrade, ServiceUpgrade, ViolatedLimit, @@ -15,17 +12,16 @@ import { export const formatNotificationText = (project: Project, notification: Notification) => { const projectName = project.name - const notificationName = notification.notification_name - if (notificationName === NotificationName.ProjectExceedingTierLimit) { - const { violations } = notification.data as ProjectExceedingTierLimitData + if (notification.data.name === NotificationName.ProjectExceedingTierLimit) { + const { violations } = notification.data const violationsText = violations .map((violation: ViolatedLimit) => violation.dimension as string) .reduce((a: string, b: string) => `${a}, ${b}`) return `Your project "${projectName}" has exceeded its limits in the following areas: ${violationsText}.` - } else if (notificationName === NotificationName.PostgresqlUpgradeAvailable) { - const { upgrade_type, additional } = notification.data as PostgresqlUpgradeData + } else if (notification.data.name === NotificationName.PostgresqlUpgradeAvailable) { + const { upgrade_type, additional } = notification.data if (upgrade_type === 'postgresql-server') { const { version_to } = additional as ServerUpgrade @@ -35,24 +31,26 @@ export const formatNotificationText = (project: Project, notification: Notificat return `New version of "${name}" (${version_to}) is now available for project "${projectName}".` } return '' - } else if (notificationName === NotificationName.PostgresqlUpgradeCompleted) { - const { upgrade_type, additional } = notification.data as PostgresqlUpgradeData + } else if (notification.data.name === NotificationName.PostgresqlUpgradeCompleted) { + const { upgrade_type, additional } = notification.data if (upgrade_type === 'postgresql-server') { const { version_to } = additional as ServerUpgrade return `Postgres (${version_to}) has been successfully updated to ${version_to} for project "${projectName}".` } else if (upgrade_type === 'extensions') { const { name, version_to } = additional as ExtensionsUpgrade - return `The extension "${name}" has been successfully updated to ${version_to} for project "${projectName}` + return `The extension "${name}" has been successfully updated to ${version_to} for project "${projectName}".` } - } else if (notificationName === 'project.update-completed') { - const { upgrades } = notification.data as ProjectUpdateData + } else if (notification.data.name === NotificationName.ProjectUpdateCompleted) { + const { upgrades } = notification.data const upgradesText = upgrades .map((upgrade: ServiceUpgrade) => upgrade.name) .reduce((a: string, b: string) => `${a}, ${b}`) - return `The following services have been successfully updated for project "${projectName}": ${upgradesText}` + return `The following services have been successfully updated for project "${projectName}": ${upgradesText}.` + } else if (notification.data.name === NotificationName.ProjectInformational) { + return notification.data.message } else { - return `Unknown notification type: ${notification.notification_name} - Please reach out to support for more information` + return `Unknown notification type: ${notification.notification_name} - Please reach out to support for more information.` } } @@ -65,6 +63,8 @@ export const formatNotificationCTAText = (availableActions: Action[]) => { return 'Restart your project to get the latest updates.' case ActionType.UpgradeProjectToPro: return 'Upgrade your project to ensure continued availability.' + case ActionType.PgBouncerRestart: + return 'Restart your connection pooler to get the latest updates.' default: return '' } diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx index d29b63e6eea..b1edc82c6ae 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx @@ -1,7 +1,7 @@ import { FC, Fragment, useState } from 'react' import { useRouter } from 'next/router' import { Button, IconBell, Popover } from '@supabase/ui' -import { Notification, NotificationStatus } from '@supabase/shared-types/out/notifications' +import { Notification, NotificationStatus, ActionType } from '@supabase/shared-types/out/notifications' import { Project } from 'types' import { useNotifications, useStore } from 'hooks' @@ -44,12 +44,21 @@ const NotificationsPopover: FC = () => { if (!projectToRestart || !targetNotification) return const { id } = targetNotification + const { id: projectId, ref, region } = projectToRestart + const serviceNamesByActionName: Record = { + [ActionType.PgBouncerRestart]: 'pgbouncer', + [ActionType.SchedulePostgresRestart]: 'postgresql' + } + const services: string[] = targetNotification.meta.actions_available + .map((action) => action.action_type) + .filter((actionName) => Object.keys(serviceNamesByActionName).indexOf(actionName) !== -1) + .map((actionName) => serviceNamesByActionName[actionName]) const { error } = await post(`${API_URL}/projects/${ref}/restart-services`, { restartRequest: { region, source_notification_id: id, - services: ['postgresql'], + services: services, }, }) @@ -61,7 +70,7 @@ const NotificationsPopover: FC = () => { }) } else { app.onProjectPostgrestStatusUpdated(projectId, 'OFFLINE') - ui.setNotification({ category: 'success', message: `Restarting project` }) + ui.setNotification({ category: 'success', message: `Restarting services` }) router.push(`/project/${ref}`) } diff --git a/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx b/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx index 098176af0a1..57dac2a1607 100644 --- a/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx +++ b/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx @@ -1,8 +1,9 @@ import { FC, ReactNode, useEffect } from 'react' import { observer } from 'mobx-react-lite' -import { useStore } from 'hooks' +import { useStore, withAuth } from 'hooks' import Error from 'components/ui/Error' +import { useSqlEditorStore, SqlEditorContext } from 'localStores/sqlEditor/SqlEditorStore' import ProjectLayout from '../ProjectLayout/ProjectLayout' import SQLEditorMenu from './SQLEditorMenu' @@ -12,26 +13,36 @@ interface Props { } const SQLEditorLayout: FC = ({ title, children }) => { - const { content } = useStore() + const { ui, content, meta } = useStore() + const { profile: user } = ui + + const sqlEditorStore = useSqlEditorStore(ui.selectedProject?.ref, meta) useEffect(() => { - content.load() - }, []) + if (sqlEditorStore) { + // this calls content.load() for us, as well as loading tabs + sqlEditorStore.loadRemotePersistentData(content, user?.id) + } + }, [sqlEditorStore]) - return content.error ? ( - - - - ) : ( - } - > - {children} - + return ( + + {content.error ? ( + + + + ) : ( + } + > + {children} + + )} + ) } -export default observer(SQLEditorLayout) +export default withAuth(observer(SQLEditorLayout)) diff --git a/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx index 3781412fd73..b8f0c135734 100644 --- a/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx +++ b/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx @@ -1,5 +1,4 @@ import { useState } from 'react' -import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' import { Button, @@ -12,19 +11,16 @@ import { IconLoader, IconTrash, IconChevronDown, - Loading, Modal, IconEdit2, } from '@supabase/ui' import { IS_PLATFORM } from 'lib/constants' -import { useStore } from 'hooks' -import { useProjectContentStore } from 'stores/projectContentStore' +import { useOptimisticSqlSnippetCreate, useStore } from 'hooks' import QueryTab from 'localStores/sqlEditor/QueryTab' import { useSqlStore, TAB_TYPES } from 'localStores/sqlEditor/SqlEditorStore' import RenameQuery from 'components/to-be-cleaned/SqlEditor/RenameQuery' -import { createSqlSnippet } from 'components/to-be-cleaned/SqlEditor/SqlEditor.utils' import ConfirmationModal from 'components/ui/ConfirmationModal' import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem' @@ -46,11 +42,12 @@ const OpenQueryItem = observer(({ tabInfo }: { tabInfo: QueryTab }) => { }) const DropdownMenu = observer(({ tabInfo }: { tabInfo: QueryTab }) => { - const router = useRouter() - const { ref } = router.query + const { + ui: { profile: user }, + content: contentStore, + } = useStore() const sqlEditorStore: any = useSqlStore() - const contentStore: any = useProjectContentStore(ref) const [tabId, setTabId] = useState('') const [renameModalOpen, setRenameModalOpen] = useState(false) @@ -109,9 +106,14 @@ const DropdownMenu = observer(({ tabInfo }: { tabInfo: QueryTab }) => { buttonLabel="Confirm" visible={deleteModalOpen} onSelectConfirm={async () => { - await contentStore.del(id) - await contentStore.load() sqlEditorStore.closeTab(id) + + await contentStore.del(id) + + sqlEditorStore.loadTabs( + sqlEditorStore.tabsFromContentStore(contentStore, user?.id), + false + ) }} onSelectCancel={() => setDeleteModalOpen(false)} > @@ -124,39 +126,11 @@ const DropdownMenu = observer(({ tabInfo }: { tabInfo: QueryTab }) => { }) const SideBarContent = observer(() => { - const router = useRouter() - const { ref } = router.query - - const { ui } = useStore() - const { profile: user } = ui - const sqlEditorStore: any = useSqlStore() - const contentStore: any = useProjectContentStore(ref) const [filterString, setFilterString] = useState('') - const [loadingNewQuery, setLoadingNewQuery] = useState(false) - async function handleNewQuery() { - try { - setLoadingNewQuery(true) - // create new sql snippet, this also reloads the project_content store - // @ts-ignore - const snippet = await createSqlSnippet({ router }) - - // reload the local sqlEditorStore - await sqlEditorStore.loadRemotePersistentData(contentStore, user?.id) - - // select tab with new snippet - if (snippet) sqlEditorStore.selectTab(snippet.id) - setLoadingNewQuery(false) - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to create new query: ${error.message}`, - }) - setLoadingNewQuery(false) - } - } + const handleNewQuery = useOptimisticSqlSnippetCreate() const getStartedTabs = (sqlEditorStore?.tabs ?? []).filter((tab: any) => { return tab.type === TAB_TYPES.WELCOME @@ -191,7 +165,6 @@ const SideBarContent = observer(() => { type="default" style={{ justifyContent: 'start' }} onClick={() => handleNewQuery()} - loading={loadingNewQuery} > New query @@ -237,37 +210,35 @@ const SideBarContent = observer(() => { })}
    )} - -
    - {favouriteTabs.length >= 1 && ( -
    - -
    - {favouriteTabs.map((tabInfo: any) => { - const { id } = tabInfo || {} - return - })} -
    +
    + {favouriteTabs.length >= 1 && ( +
    + +
    + {favouriteTabs.map((tabInfo: any) => { + const { id } = tabInfo || {} + return + })}
    - )} - {queryTabs.length >= 1 && ( -
    - -
    - {queryTabs.map((tabInfo: any) => { - const { id } = tabInfo || {} - return - })} -
    +
    + )} + {queryTabs.length >= 1 && ( +
    + +
    + {queryTabs.map((tabInfo: any) => { + const { id } = tabInfo || {} + return + })}
    - )} - {filterString.length > 0 && favouriteTabs.length === 0 && queryTabs.length === 0 && ( -
    -

    No queries found

    -
    - )} -
    - +
    + )} + {filterString.length > 0 && favouriteTabs.length === 0 && queryTabs.length === 0 && ( +
    +

    No queries found

    +
    + )} +
    )} diff --git a/studio/components/to-be-cleaned/SqlEditor/RenameQuery.js b/studio/components/to-be-cleaned/SqlEditor/RenameQuery.js index acdb82e1c4a..755494493ce 100644 --- a/studio/components/to-be-cleaned/SqlEditor/RenameQuery.js +++ b/studio/components/to-be-cleaned/SqlEditor/RenameQuery.js @@ -1,19 +1,13 @@ import React from 'react' import { observer } from 'mobx-react-lite' -import { useRouter } from 'next/router' import { useStore } from 'hooks' import { useSqlStore } from 'localStores/sqlEditor/SqlEditorStore' -import { useProjectContentStore } from 'stores/projectContentStore' import { Button, Form, Input, Modal } from '@supabase/ui' const RenameQuery = observer(({ tabId, onComplete, visible, onCancel }) => { - const { ui } = useStore() + const { ui, content: contentStore } = useStore() - const router = useRouter() - const { ref } = router.query - - const contentStore = useProjectContentStore(ref) const sqlEditorStore = useSqlStore() const model = prepareModel() @@ -27,10 +21,6 @@ const RenameQuery = observer(({ tabId, onComplete, visible, onCancel }) => { async function onRename(model) { try { - /* - * old localStorage save - */ - sqlEditorStore.renameQuery(tabId, model) /* * new main db save */ @@ -38,7 +28,12 @@ const RenameQuery = observer(({ tabId, onComplete, visible, onCancel }) => { name: model.name, description: model.desc, }) - await contentStore.load() + + /* + * old localStorage save + */ + sqlEditorStore.renameQuery(tabId, model) + if (onComplete) onComplete() return Promise.resolve() } catch (error) { diff --git a/studio/components/to-be-cleaned/SqlEditor/SqlEditor.constants.ts b/studio/components/to-be-cleaned/SqlEditor/SqlEditor.constants.ts index 6bfa26446d5..c8b9ae578e4 100644 --- a/studio/components/to-be-cleaned/SqlEditor/SqlEditor.constants.ts +++ b/studio/components/to-be-cleaned/SqlEditor/SqlEditor.constants.ts @@ -1,8 +1,8 @@ -import { UserContent } from 'types' +import { SqlSnippets, UserContent } from 'types' export const SQL_SNIPPET_SCHEMA_VERSION = '1.0' -export const NEW_SQL_SNIPPET_SKELETON: UserContent = { +export const NEW_SQL_SNIPPET_SKELETON: UserContent = { name: 'New Query', description: '', type: 'sql', diff --git a/studio/components/to-be-cleaned/SqlEditor/SqlEditor.utils.ts b/studio/components/to-be-cleaned/SqlEditor/SqlEditor.utils.ts index 3faa21a16c2..7ba8d40a516 100644 --- a/studio/components/to-be-cleaned/SqlEditor/SqlEditor.utils.ts +++ b/studio/components/to-be-cleaned/SqlEditor/SqlEditor.utils.ts @@ -1,53 +1,23 @@ -import { UserContent } from 'types' -import { useProjectContentStore } from 'stores/projectContentStore' +import { SqlSnippets, UserContent } from 'types' import { NEW_SQL_SNIPPET_SKELETON } from './SqlEditor.constants' -/* Creates a new SQL snippet with a basic skeleton */ -export const createSqlSnippet = async ({ - router, - sql, +export const createSqlSnippetSkeleton = ({ name, + sql, + owner_id, }: { - router: any - sql: any - name: any -}) => { - const { ref } = router.query - const contentStore = useProjectContentStore(ref) - const skeleton: UserContent = { + name?: string + sql?: string + owner_id?: number +} = {}): UserContent => { + return { ...NEW_SQL_SNIPPET_SKELETON, ...(name && { name }), + ...(owner_id && { owner_id }), content: { ...NEW_SQL_SNIPPET_SKELETON.content, content_id: '', - sql: sql, + sql: sql ?? '', }, } - - const { data: sqlSnippet, error } = await contentStore.create(skeleton) - - if (error) throw error - - // reload store data - await contentStore.load() - return sqlSnippet[0] -} - -/* - * deleteSqlSnippet() - * - * Deletes a SQL snippet - * - */ -export const deleteSqlSnippet = async ({ router, id }: { router: any; id: any }) => { - const { ref } = router.query - const contentStore = useProjectContentStore(ref) - - const { data: delReport, error } = await contentStore.del(id) - if (error) throw error - - // reload store data - await contentStore.load() - - return delReport } diff --git a/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js b/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js index ff4da97216d..72e89d54139 100644 --- a/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js +++ b/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js @@ -1,32 +1,34 @@ -import React, { useEffect, useRef, useState } from 'react' -import { observer } from 'mobx-react-lite' -import { useRouter } from 'next/router' -import DataGrid from '@supabase/react-data-grid' import Split from 'react-split' +import Editor from '@monaco-editor/react' +import DataGrid from '@supabase/react-data-grid' +import { toJS } from 'mobx' import { CSVLink } from 'react-csv' +import { debounce } from 'lodash' +import { observer } from 'mobx-react-lite' +import { useEffect, useRef, useState } from 'react' import { Button, - IconHeart, + Dropdown, + IconAlertCircle, + IconCheck, IconChevronDown, IconChevronUp, + IconHeart, + IconLoader, + IconRefreshCcw, Typography, - Dropdown, } from '@supabase/ui' -import Editor from '@monaco-editor/react' +import * as Tooltip from '@radix-ui/react-tooltip' -import { copyToClipboard, timeout } from 'lib/helpers' +import { useKeyboardShortcuts, useStore, useWindowDimensions, usePrevious } from 'hooks' +import Telemetry from 'lib/telemetry' import { IS_PLATFORM } from 'lib/constants' +import { copyToClipboard, timeout } from 'lib/helpers' import { useSqlStore, UTILITY_TAB_TYPES } from 'localStores/sqlEditor/SqlEditorStore' -import { useProjectContentStore } from 'stores/projectContentStore' - import { SQL_SNIPPET_SCHEMA_VERSION } from './SqlEditor.constants' -import Telemetry from 'lib/telemetry' -import { useStore } from 'hooks' -import { debounce } from 'lodash' -import { useWindowDimensions, useKeyboardShortcuts } from 'hooks' - const TabSqlQuery = observer(() => { + const { content: contentStore } = useStore() const sqlEditorStore = useSqlStore() const { height: screenHeight } = useWindowDimensions() const snapOffset = 50 @@ -49,8 +51,23 @@ const TabSqlQuery = observer(() => { sqlEditorStore.activeTab.setSplitSizes(sizes) } + async function updateSqlSnippet(value) { + if (sqlEditorStore.activeTab) { + await contentStore.updateSql(sqlEditorStore.activeTab.id, { + content: { + schema_version: SQL_SNIPPET_SCHEMA_VERSION, + content_id: sqlEditorStore.activeTab.id, + sql: value, + favorite: sqlEditorStore.activeTab.favorite, + }, + }) + } else { + console.warn('No active tab found while updating SQL snippet') + } + } + return ( -
    +
    { collapsed={sqlEditorStore.activeTab.utilityTabHeight == 0 ? 1 : undefined} onDragEnd={onDragEnd} > - +
    - +
    @@ -72,12 +92,7 @@ const TabSqlQuery = observer(() => { }) export default TabSqlQuery -const MonacoEditor = ({ error }) => { - const router = useRouter() - const { ref } = router.query - - const contentStore = useProjectContentStore(ref) - +const MonacoEditor = ({ error, updateSqlSnippet }) => { const sqlEditorStore = useSqlStore() const editorRef = useRef(null) const monacoRef = useRef(null) @@ -154,6 +169,9 @@ const MonacoEditor = ({ error }) => { editor?.focus() } + // changes are stored in debouncer before running persistData() + let debounceUpdateSqlSnippet = debounce((value) => updateSqlSnippet(value), 1500) + async function handleEditorChange(value) { // update sqlEditorStore with new value immediately // this is so any SQL run will be whatever is currently in monaco editor @@ -163,29 +181,8 @@ const MonacoEditor = ({ error }) => { debounceUpdateSqlSnippet(value) } - // changes are stored in debouncer before running persistData() - let debounceUpdateSqlSnippet = debounce((value) => updateSqlSnippet(value), 1500) - - async function updateSqlSnippet(value) { - // - // this will need to be refactored but for now - // we still use the old sqlEditorStore to handle db updates - if (sqlEditorStore.activeTab) { - await contentStore.updateSql(sqlEditorStore.activeTab.id, { - content: { - schema_version: SQL_SNIPPET_SCHEMA_VERSION, - content_id: sqlEditorStore.activeTab.id, - sql: value, - favorite: sqlEditorStore.activeTab.favorite, - }, - }) - } else { - console.warn('No active tab found while updating SQL snippet') - } - } - return ( -
    +
    { ) } -const UtilityPanel = observer(() => { +const UtilityPanel = observer(({ updateSqlSnippet }) => { const sqlEditorStore = useSqlStore() return ( <>
    -
    - +
    +
    @@ -271,7 +268,101 @@ const ResultsDropdown = observer(() => { ) }) -const UtilityActions = observer(() => { +const SavingIndicator = observer(({ updateSqlSnippet }) => { + const { content } = useStore() + const sqlEditorStore = useSqlStore() + const previousState = usePrevious(content.savingState) + const [showSavedText, setShowSavedText] = useState(false) + + useEffect(() => { + let cancel = false + + if ( + (previousState === 'CREATING' || previousState === 'UPDATING') && + content.savingState === 'IDLE' + ) { + setShowSavedText(true) + setTimeout(() => { + if (!cancel) setShowSavedText(false) + }, 3000) + } + + return () => (cancel = true) + }, [content.savingState]) + + const retry = () => { + const [item] = content.list((item) => item.id === sqlEditorStore.selectedTabId) + + if (content.savingState === 'CREATING_FAILED' && item) { + content.save(toJS(item)) + } + + if (content.savingState === 'UPDATING_FAILED' && item) { + updateSqlSnippet(sqlEditorStore.activeTab.query) + } + } + + return ( +
    + {(content.savingState === 'CREATING_FAILED' || content.savingState === 'UPDATING_FAILED') && ( + + )} + {(content.savingState === 'CREATING' || content.savingState === 'UPDATING') && ( + + + + + + +
    + Saving changes... +
    +
    +
    + )} + {(content.savingState === 'CREATING_FAILED' || content.savingState === 'UPDATING_FAILED') && ( + + )} + {showSavedText && ( + + + + + + +
    + All changes saved +
    +
    +
    + )} + + {content.savingState === 'CREATING_FAILED' && 'Failed to create'} + {content.savingState === 'UPDATING_FAILED' && 'Failed to save'} + +
    + ) +}) + +const UtilityActions = observer(({ updateSqlSnippet }) => { const sqlEditorStore = useSqlStore() useKeyboardShortcuts( @@ -291,6 +382,7 @@ const UtilityActions = observer(() => { return ( <> + {IS_PLATFORM && } diff --git a/studio/hooks/index.ts b/studio/hooks/index.ts index 87e81884c1f..22e133f6a13 100644 --- a/studio/hooks/index.ts +++ b/studio/hooks/index.ts @@ -1,5 +1,6 @@ export * from './misc/useStore' export * from './misc/withAuth' +export * from './misc/useOptimisticSqlSnippetCreate' export * from './misc/useNotifications' export * from './deprecated' export * from './queries' diff --git a/studio/hooks/misc/useOptimisticSqlSnippetCreate.ts b/studio/hooks/misc/useOptimisticSqlSnippetCreate.ts new file mode 100644 index 00000000000..60ebaa40046 --- /dev/null +++ b/studio/hooks/misc/useOptimisticSqlSnippetCreate.ts @@ -0,0 +1,40 @@ +import { createSqlSnippetSkeleton } from 'components/to-be-cleaned/SqlEditor/SqlEditor.utils' +import { useSqlStore } from 'localStores/sqlEditor/SqlEditorStore' +import { useCallback } from 'react' +import { useStore } from './useStore' + +export function useOptimisticSqlSnippetCreate() { + const { ui, content: contentStore } = useStore() + const { profile: user } = ui + + const sqlEditorStore: any = useSqlStore() + + return useCallback( + async (args?: Parameters[0]) => { + // get currently selected tab id in case we need to roll back to it + const previouslySelectedTabId = sqlEditorStore.selectedTabId + + const snippet = createSqlSnippetSkeleton({ owner_id: user?.id, ...args }) + + // save the snippet in memory in the content store with a client-generated id + const { data } = contentStore.createOptimistically(snippet) + + // update the tabs in the sql editor storage to match the content store + const tabs = sqlEditorStore.tabsFromContentStore(contentStore, user?.id) + sqlEditorStore.loadTabs(tabs, false) + + // select tab with new snippet + sqlEditorStore.selectTab(data.id) + + // save the new query to the server in the background + const { error } = await contentStore.save(data) + if (error) { + ui.setNotification({ + category: 'error', + message: `Failed to create new query: ${error.message}`, + }) + } + }, + [user, sqlEditorStore, contentStore] + ) +} diff --git a/studio/lib/helpers.ts b/studio/lib/helpers.ts index c693d81aa16..ec95a3b4eb2 100644 --- a/studio/lib/helpers.ts +++ b/studio/lib/helpers.ts @@ -1,3 +1,4 @@ +import { v4 as _uuidV4 } from 'uuid' import { post } from 'lib/common/fetch' import { PASSWORD_STRENGTH, DEFAULT_MINIMUM_PASSWORD_STRENGTH, API_URL } from 'lib/constants' @@ -32,14 +33,8 @@ export const prettifyJSON = (minifiedJSON: string) => { } } -// https://stackoverflow.com/a/2117523 -// a "good enough" unique ID that typescript is happy with, and doesn't have external dependencies export const uuidv4 = () => { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = (Math.random() * 16) | 0, - v = c == 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) + return _uuidV4() } export const timeout = (ms: number) => { diff --git a/studio/localStores/sqlEditor/SqlEditorStore.js b/studio/localStores/sqlEditor/SqlEditorStore.js index 47adbd3cfd2..7b05598377d 100644 --- a/studio/localStores/sqlEditor/SqlEditorStore.js +++ b/studio/localStores/sqlEditor/SqlEditorStore.js @@ -5,17 +5,23 @@ import { IS_PLATFORM } from 'lib/constants' import Tab from './Tab' import QueryTab from './QueryTab' import Favorite from './Favorite' -// import { useStore } from 'hooks' import { SchemasQuery, TableColumnsQuery, AllFunctionsQuery } from './queries' import { isUndefined } from 'lodash' let store = null let projectRef = null +let metaProjectRef = null export function useSqlEditorStore(ref, meta) { - if (store === null || ref !== projectRef) { + if ((ref && ref !== projectRef) || (meta?.projectRef && meta.projectRef !== metaProjectRef)) { projectRef = ref + metaProjectRef = meta.projectRef store = new SqlEditorStore(ref, meta) } + + if (!ref || !meta) { + store = null + } + return store } @@ -70,6 +76,10 @@ class SqlEditorStore { } get activeTab() { + if (this.tabs.length === 1) { + this.selectedTabId = this.tabs[0].id + } + const found = this.tabs.find((x) => x.id === this.selectedTabId) return found } @@ -112,22 +122,16 @@ class SqlEditorStore { else return tab.isExecuting } - /* - * ! Temporary solution ! - * - * New action for retriving data from main db user_content - * You need to pass the contentStore into this - */ - async loadRemotePersistentData(contentStore, user_id) { - await contentStore.load() - const sqlSnippets = contentStore.sqlSnippets((x) => x.owner_id === user_id) - const snippets = toJS(sqlSnippets) + tabsFromContentStore(contentStore, user_id) { + const snippets = contentStore + .sqlSnippets((x) => x.owner_id === user_id) + .map((snippet) => toJS(snippet)) // add the welcome tab let tabs = IS_PLATFORM ? [new Tab('Welcome', TAB_TYPES.WELCOME)] : [] // add the tabs to array, but with structure the localStore expects - snippets.map((snippet) => { + snippets.forEach((snippet) => { const data = { desc: snippet.description, id: snippet.id, @@ -136,9 +140,24 @@ class SqlEditorStore { type: 'SQL_QUERY', favorite: snippet.content.favorite, } + tabs.push(data) }) + return tabs + } + + /* + * ! Temporary solution ! + * + * New action for retriving data from main db user_content + * You need to pass the contentStore into this + */ + async loadRemotePersistentData(contentStore, user_id) { + await contentStore.load() + + const tabs = this.tabsFromContentStore(contentStore, user_id) + /* * Reshape snippet content to fit the SqlEditorStore shape */ @@ -197,7 +216,7 @@ class SqlEditorStore { this.selectedTabId = this.tabs.length ? this.tabs[0].id : undefined } - loadTabs(values) { + loadTabs(values, autoSelectTab = true) { const tabs = values.map((x) => { switch (x.type) { case TAB_TYPES.WELCOME: @@ -213,7 +232,10 @@ class SqlEditorStore { }) this.tabs = tabs.filter((x) => x !== undefined) - this.selectedTabId = this.tabs.length ? this.tabs[0].id : undefined + + if (autoSelectTab) { + this.selectedTabId = this.tabs.length ? this.tabs[0].id : undefined + } } loadFavorites(values) { @@ -279,10 +301,15 @@ class SqlEditorStore { } closeTab(id) { - this.tabs = this.tabs.filter((x) => x.id !== id) + const tabs = this.tabs.filter((x) => x.id !== id) // if user close selectedTab, select the last tab if available - if (this.tabs.length && this.selectedTabId === id) - this.selectedTabId = this.tabs[this.tabs.length - 1].id + if (tabs.length && this.selectedTabId === id) { + const nextId = tabs[tabs.length - 1].id + + if (nextId) { + this.selectedTabId = nextId + } + } } createQueryTab(query, name) { @@ -331,10 +358,10 @@ class SqlEditorStore { } renameQuery(id, model) { - const found = this.tabs.find((x) => x.id == id) + const found = this.tabs.find((x) => x.id === id) found?.rename(model) - const favorite = this.favorites.find((x) => x.key == id) + const favorite = this.favorites.find((x) => x.key === id) favorite?.rename(model) } diff --git a/studio/package-lock.json b/studio/package-lock.json index 6def253517d..346948ff30f 100644 --- a/studio/package-lock.json +++ b/studio/package-lock.json @@ -20,7 +20,7 @@ "@stripe/stripe-js": "^1.25.0", "@supabase/postgres-meta": "^0.26.1", "@supabase/react-data-grid": "^7.1.0-beta.7", - "@supabase/shared-types": "^0.1.21", + "@supabase/shared-types": "^0.1.24", "@supabase/supabase-js": "^1.35.3", "@supabase/ui": "0.37.0-alpha.58", "ajv": "^8.6.3", @@ -8338,9 +8338,9 @@ } }, "node_modules/@supabase/shared-types": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.21.tgz", - "integrity": "sha512-iF6XMy2cYPm7p3hximwuDmQOFZuVHVKr/uKcrtQlFrQIYjmMMrF/7Qm20muq2+oH8WNxcj29AZJ6oBx6mYzEqQ==" + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.24.tgz", + "integrity": "sha512-zvGWWLLh5ZAeiSR4VfJGBY9KbRb1Jh8//gJVmZwiaRv0i6F4i9qTsFOmWjpFgLY5+grAqwSfyIfB+jG8rxcidQ==" }, "node_modules/@supabase/storage-js": { "version": "1.7.0", @@ -35112,9 +35112,9 @@ } }, "@supabase/shared-types": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.21.tgz", - "integrity": "sha512-iF6XMy2cYPm7p3hximwuDmQOFZuVHVKr/uKcrtQlFrQIYjmMMrF/7Qm20muq2+oH8WNxcj29AZJ6oBx6mYzEqQ==" + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.24.tgz", + "integrity": "sha512-zvGWWLLh5ZAeiSR4VfJGBY9KbRb1Jh8//gJVmZwiaRv0i6F4i9qTsFOmWjpFgLY5+grAqwSfyIfB+jG8rxcidQ==" }, "@supabase/storage-js": { "version": "1.7.0", diff --git a/studio/package.json b/studio/package.json index 3e6d9fe0220..53f50d4ebea 100644 --- a/studio/package.json +++ b/studio/package.json @@ -24,7 +24,7 @@ "@stripe/stripe-js": "^1.25.0", "@supabase/postgres-meta": "^0.26.1", "@supabase/react-data-grid": "^7.1.0-beta.7", - "@supabase/shared-types": "^0.1.21", + "@supabase/shared-types": "^0.1.24", "@supabase/supabase-js": "^1.35.3", "@supabase/ui": "0.37.0-alpha.58", "ajv": "^8.6.3", @@ -74,6 +74,7 @@ "swr": "^1.2.1", "uniforms-bootstrap4": "^3.6.2", "uniforms-bridge-json-schema": "^3.6.2", + "uuid": "^8.3.2", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -104,6 +105,7 @@ "@types/react-window-infinite-loader": "^1.0.5", "@types/semver": "^7.3.9", "@types/sqlstring": "^2.3.0", + "@types/uuid": "^8.3.4", "@types/zxcvbn": "^4.4.1", "autoprefixer": "^10.4.2", "babel-loader": "^8.2.3", diff --git a/studio/pages/account/associate.tsx b/studio/pages/account/associate.tsx deleted file mode 100644 index e2d8c88c7f4..00000000000 --- a/studio/pages/account/associate.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' -import { Button, Select } from '@supabase/ui' -import { observer } from 'mobx-react-lite' - -import { useStore } from 'hooks' -import { API_URL } from 'lib/constants' -import { get, post } from 'lib/common/fetch' - -const Associate = observer(() => { - const { ui, app } = useStore() - const sortedOrganizations = app.organizations.list() - - const [awsMarketplace, setAwsMarketplace] = useState() - const [error, setError] = useState('') - const [loading, setLoading] = useState(true) - const [organizationSlug, setOrganizationSlug] = useState('') - const [saving, setSaving] = useState(false) - const router = useRouter() - - const handleChange = (e: any) => { - setOrganizationSlug(e.target.value) - } - - const onSave = async () => { - if (organizationSlug === '') { - setError('Please select an organization') - return - } - setSaving(true) - const { error } = await post( - `${API_URL}/organizations/${organizationSlug}/associate`, - awsMarketplace - ) - if (error) { - setError(error.message) - } - setSaving(false) - if (!error) { - router.push('/') - } - } - - useEffect(() => { - const getAwsMarketplaceInfo = async () => { - try { - setLoading(true) - const xAmznMarketplaceToken = router.query['x-amzn-marketplace-token'] - if (typeof xAmznMarketplaceToken !== 'string') { - throw new Error('Missing or multiple x-amzn-marketplace-token') - } - const response = await get( - `${API_URL}/organizations/aws-marketplace?x-amzn-marketplace-token=${encodeURIComponent( - xAmznMarketplaceToken - )}` - ) - if (response.error) { - throw response.error - } - if (response.slug) { - setOrganizationSlug(response.slug) - } - setAwsMarketplace(response.awsMarketplace) - setLoading(false) - if (sortedOrganizations.length === 1) { - onSave() - } - } catch (error: any) { - ui.setNotification({ - category: 'error', - message: `Failed to get AWS Marketplace Information: ${error.message}`, - }) - } - } - getAwsMarketplaceInfo() - }, []) - - return ( -
    - {!loading && ( -
    - - -
    - )} -
    - ) -}) - -export default Associate diff --git a/studio/pages/index.tsx b/studio/pages/index.tsx index c913013180d..45826dc1282 100644 --- a/studio/pages/index.tsx +++ b/studio/pages/index.tsx @@ -105,9 +105,7 @@ Home.getLayout = (page) => {page} export default observer(Home) // detect for redirect from 3rd party service like vercel, aws... -function isRedirectFromThirdPartyService(router: NextRouter) { - return router.query.next !== undefined || router.query['x-amzn-marketplace-token'] !== undefined -} +const isRedirectFromThirdPartyService = (router: NextRouter) => router.query.next !== undefined const UnauthorizedLanding = () => { const router = useRouter() @@ -148,8 +146,6 @@ const IndexLayout = withAuth( router.push(`/vercel/integrate?${params.toString()}`) } else if (router.query?.next?.includes('new-project')) { router.push('/new/project') - } else if (router.query['x-amzn-marketplace-token'] != undefined) { - router.push(`/account/associate?${params.toString()}`) } else if ( typeof router.query?.next === 'string' && router.query?.next?.startsWith('project/_/') diff --git a/studio/pages/project/[ref]/sql/index.tsx b/studio/pages/project/[ref]/sql/index.tsx index 21e61263935..d25e7de4522 100644 --- a/studio/pages/project/[ref]/sql/index.tsx +++ b/studio/pages/project/[ref]/sql/index.tsx @@ -1,58 +1,16 @@ import React, { useEffect, useState } from 'react' import { observer } from 'mobx-react-lite' import { useMonaco } from '@monaco-editor/react' -import { - useSqlEditorStore, - SqlEditorContext, - useSqlStore, - TAB_TYPES, -} from 'localStores/sqlEditor/SqlEditorStore' +import { useSqlStore, TAB_TYPES } from 'localStores/sqlEditor/SqlEditorStore' 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 { useStore } from 'hooks' import { SQLEditorLayout } from 'components/layouts' -import BaseLayout from 'components/layouts' +import { NextPageWithLayout } from 'types' -const PageConfig = () => { - const { meta, ui } = useStore() - const { profile: user } = ui - - useEffect(() => { - if (ui.selectedProject !== undefined) { - loadPersistantData() - } - }, [ui.selectedProject]) - - /* - * Load persited data - */ - async function loadPersistantData() { - if (sqlEditorStore === undefined) return - 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 SqlEditor = observer(() => { +const SqlEditor: NextPageWithLayout = () => { const { meta, ui } = useStore() const { isDarkTheme } = ui const sqlEditorStore: any = useSqlStore() @@ -142,4 +100,8 @@ const SqlEditor = observer(() => { } return <>{renderTabContent()} -}) +} + +SqlEditor.getLayout = (page) => {page} + +export default observer(SqlEditor) diff --git a/studio/stores/content/ProjectContentStore.ts b/studio/stores/content/ProjectContentStore.ts index e70b3c83329..391b93ec93b 100644 --- a/studio/stores/content/ProjectContentStore.ts +++ b/studio/stores/content/ProjectContentStore.ts @@ -1,6 +1,7 @@ import { makeAutoObservable } from 'mobx' import { keyBy } from 'lodash' +import { uuidv4 } from 'lib/helpers' import { get, post, patch, delete_ } from 'lib/common/fetch' import { LogSqlSnippets, UserContent, UserContentMap } from 'types' import { IRootStore } from '../RootStore' @@ -14,15 +15,22 @@ export interface IProjectContentStore { isLoading: boolean isInitialized: boolean isLoaded: boolean + savingState: 'IDLE' | 'CREATING' | 'CREATING_FAILED' | 'UPDATING' | 'UPDATING_FAILED' error: any recentLogSqlSnippets: LogSqlSnippets.Content[] baseUrl: string projectRef?: string - load: () => void + load: () => Promise loadPersistentData: () => Promise - create: (x: UserContent) => { data: UserContent; error: { error: { message: string } } } + save: ( + content: UserContent + ) => Promise<{ data: UserContent; error: null } | { data: null; error: { message: string } }> + create: ( + content: UserContent + ) => Promise<{ data: UserContent; error: null } | { data: null; error: { message: string } }> + createOptimistically: (content: UserContent) => { data: UserContent; error: null } list: (filter?: any) => any[] reports: (filter?: any) => any[] sqlSnippets: (filter?: any) => any[] @@ -30,6 +38,23 @@ export interface IProjectContentStore { addRecentLogSqlSnippet: (snippet: Partial) => void clearRecentLogSqlSnippets: () => void setProjectRef: (ref?: string) => void + update: ( + id: any, + updates: any, + type: UserContent['type'] + ) => Promise< + | { + data: UserContent + error: null + } + | { + data: null + error: unknown + } + > + + del(id: any): Promise<{ data: boolean; error: unknown }> + delOptimistically(id: string): { data: boolean; error: null } } export default class ProjectContentStore implements IProjectContentStore { @@ -51,6 +76,7 @@ export default class ProjectContentStore implements IProjectContentStore { recentLogSqlSnippets: LogSqlSnippets.Content[] = [] state = this.STATES.INITIAL + savingState: 'IDLE' | 'CREATING' | 'CREATING_FAILED' | 'UPDATING' | 'UPDATING_FAILED' error = null constructor(rootStore: IRootStore, options: { projectRef: string }) { @@ -61,6 +87,7 @@ export default class ProjectContentStore implements IProjectContentStore { this.recentLogSqlKey = `${this.localStorageKey}-recent-log-sql` this.loadPersistentData() this.baseUrl = `` + this.savingState = 'IDLE' makeAutoObservable(this) } @@ -84,7 +111,7 @@ export default class ProjectContentStore implements IProjectContentStore { if (response.error) { throw response.error } - this.data = keyBy(response, 'id') + this.data = keyBy(response.data ?? response, 'id') return response } @@ -191,37 +218,84 @@ export default class ProjectContentStore implements IProjectContentStore { return snippets.sort((a, b) => a.name.localeCompare(b.name)) } - // @ts-ignore - async create(payload: any) { + /** + * Creates a new content item, but does not update local store + */ + async save(payload: UserContent) { try { + this.savingState = 'CREATING' + const headers = { 'Content-Type': 'application/json', } - const created = await post(this.baseUrl, payload, { headers }) + const created = await post(this.baseUrl, payload, { headers }) if (created.error) throw created.error - this.data[created['id']] = created - return { data: created, error: null } + + this.savingState = 'IDLE' + + return { data: created as UserContent, error: null } } catch (error) { - return { data: null, error } + this.savingState = 'CREATING_FAILED' + + return { data: null, error: error as { message: string } } } } - async update(id: any, updates: any, type: UserContent['type']) { + /** + * Creates a new content item, and updates local store + */ + async create(payload: UserContent) { + const { data, error } = await this.save(payload) + + if (error) { + return { data: null, error } + } + + if (data && data['id']) { + this.data[data['id']] = data + } + + return { data: data as UserContent, error: null } + } + + /** + * Creates a new content item locally in store without saving to the server + */ + createOptimistically(payload: UserContent) { + const created = { ...payload, id: uuidv4() } + + this.data[created.id] = created + + return { data: created, error: null } + } + + async update(id: any, updates: any, type?: UserContent['type']) { try { + this.savingState = 'UPDATING' + const headers = { 'Content-Type': 'application/json', } let payload = { ...updates, - type, id, } + if (type) { + payload.type = type + } + const url = `${this.baseUrl}?id=${id}` - const updated = await patch(url, payload, { headers }) + const updated = await patch(url, payload, { headers }) if (updated.error) throw updated.error - this.data[id] = updated - return { data: updated, error: null } + + const localUpdate = { ...this.data[id], ...payload } + this.data[id] = localUpdate + this.savingState = 'IDLE' + + return { data: localUpdate, error: null } } catch (error) { + this.savingState = 'UPDATING_FAILED' + return { data: null, error } } } @@ -249,6 +323,11 @@ export default class ProjectContentStore implements IProjectContentStore { } } + delOptimistically(id: string) { + delete this.data[id] + return { data: true, error: null } + } + setProjectRef(ref?: string) { if (ref) { this.projectRef = ref diff --git a/studio/stores/pgmeta/MetaStore.ts b/studio/stores/pgmeta/MetaStore.ts index 33d6f852ebb..874bfa65ada 100644 --- a/studio/stores/pgmeta/MetaStore.ts +++ b/studio/stores/pgmeta/MetaStore.ts @@ -61,6 +61,8 @@ export interface IMetaStore { publications: IPublicationStore types: IPostgresMetaInterface + projectRef?: string + query: (value: string) => Promise validateQuery: (value: string) => Promise formatQuery: (value: string) => Promise @@ -118,6 +120,7 @@ export default class MetaStore implements IMetaStore { publications: PublicationStore types: TypesStore + projectRef?: string connectionString?: string baseUrl: string excludedSchemas = [ @@ -136,6 +139,7 @@ export default class MetaStore implements IMetaStore { constructor(rootStore: IRootStore, options: { projectRef: string; connectionString: string }) { const { projectRef, connectionString } = options this.rootStore = rootStore + this.projectRef = projectRef this.baseUrl = `${API_URL}/pg-meta/${projectRef}` const headers: any = {} diff --git a/studio/styles/main.scss b/studio/styles/main.scss index c9da32c0d5f..bbbcf1484aa 100644 --- a/studio/styles/main.scss +++ b/studio/styles/main.scss @@ -111,15 +111,6 @@ input.form-control, filter: invert(0.8); } -select { - // [Joshen] Color is hard-coded - I can't figure out how to use the UI library colors - background-image: url("data:image/svg+xml;utf8,"); -} - -select option { - @apply text-bg-primary-dark; -} - input.is-invalid { @apply bg-red-100; @apply border border-red-700; diff --git a/web/docs/guides/integrations/plasmic.mdx b/web/docs/guides/integrations/plasmic.mdx index 286339caa88..ac2e6f20b05 100644 --- a/web/docs/guides/integrations/plasmic.mdx +++ b/web/docs/guides/integrations/plasmic.mdx @@ -7,10 +7,10 @@ description: 'Get started with Supabase and Plasmic, an open-source framework fo In this guide, we will show you how to build a crowd-sourced Pokemon Pokedex, by connecting **Supabase**, an open source Firebase backend alternative, with **Plasmic**, a visual builder for the web. While many users leverage Plasmic to quickly launch and iterate on landing pages, in this tutorial we’ll show just how powerful Plasmic can be as a general-purpose visual builder for React, which can be used to design and implement fully featured read-write applications. You can play with the live demo here: -[https://plasmic-supabase-demo-git-master-plasmic.vercel.app/](https://plasmic-supabase-demo-git-master-plasmic.vercel.app/login-page) +[https://plasmic-supabase-demo.vercel.app/](https://plasmic-supabase-demo.vercel.app/) You can also take a look at the Plasmic project here: -[https://studio.plasmic.app/projects/i6YZmFxPJB69NEUgdf79ap](https://studio.plasmic.app/projects/i6YZmFxPJB69NEUgdf79ap) +[https://studio.plasmic.app/projects/66RKaSPCwKxYjCfXWHCxn6](https://studio.plasmic.app/projects/66RKaSPCwKxYjCfXWHCxn6) > You’ll need to enable 3rd-party cookies in your browser for the project to properly load. @@ -79,8 +79,8 @@ If you pre-populated the database in Step 1. you should see the following homepa Now let’s try to make some additions! The code base is currently configured to a read-only copy of the Plasmic project. Let’s make an editable copy first. -Open the default starter Plasmic project here: -[https://studio.plasmic.app/projects/i6YZmFxPJB69NEUgdf79ap](https://studio.plasmic.app/projects/i6YZmFxPJB69NEUgdf79ap) +Open the default starter Plasmic project here: +[https://studio.plasmic.app/projects/66RKaSPCwKxYjCfXWHCxn6](https://studio.plasmic.app/projects/66RKaSPCwKxYjCfXWHCxn6) ![clone-project-plasmic](/img/guides/integrations/plasmic/clone-project-plasmic-04.png) diff --git a/web/docs/guides/integrations/prisma.mdx b/web/docs/guides/integrations/prisma.mdx index bb2af7dd436..635e3bb9124 100644 --- a/web/docs/guides/integrations/prisma.mdx +++ b/web/docs/guides/integrations/prisma.mdx @@ -55,8 +55,8 @@ This project comes with TypeScript configured and has the following structure. - A `.env` file: Contains the `DATABASE_URL` variable, which Prisma will use. - A `script.ts` file: where we will run some queries using Prisma Client. This starter also comes with the following packages installed: -- [`@prisma/client`](https://www.npmjs.com/package/prisma): An auto-generated and type-safe query builder that’s _tailored_ to your data. -- [`prisma`](https://www.npmjs.com/package/@prisma/client): Prisma’s command-line interface (CLI). It allows you to initialize new project assets, generate Prisma Client, and analyze existing database structures through introspection to automatically create your application models. +- [`@prisma/client`](https://www.npmjs.com/package/@prisma/client): An auto-generated and type-safe query builder that’s _tailored_ to your data. +- [`prisma`](https://www.npmjs.com/package/prisma): Prisma’s command-line interface (CLI). It allows you to initialize new project assets, generate Prisma Client, and analyze existing database structures through introspection to automatically create your application models. > Note: Prisma works with both JavaScript and TypeScript. However, to get the best possible development experience, using TypeScript is highly recommended. ### Configuring the project to use PostgreSQL diff --git a/web/package-lock.json b/web/package-lock.json index b9cbc2c55f3..0fc3e698ae1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,7 +17,8 @@ "mermaid": "^8.13.8", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-inlinesvg": "^2.0.1" + "react-inlinesvg": "^2.0.1", + "react-loadable": "^5.5.0" }, "devDependencies": { "@types/node": "^14.17.34", @@ -8719,16 +8720,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash.assignin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" - }, - "node_modules/lodash.bind": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" - }, "node_modules/lodash.curry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", @@ -8739,66 +8730,16 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "node_modules/lodash.filter": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, "node_modules/lodash.flow": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=" }, - "node_modules/lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" - }, - "node_modules/lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "node_modules/lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" - }, - "node_modules/lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" - }, - "node_modules/lodash.reject": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" - }, - "node_modules/lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -10800,6 +10741,17 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-loadable": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-loadable/-/react-loadable-5.5.0.tgz", + "integrity": "sha512-C8Aui0ZpMd4KokxRdVAm2bQtI03k2RMRNzOB+IipV3yxFTSVICv7WoUr5L9ALB5BmKO1iHgZtWM8EvYG83otdg==", + "dependencies": { + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-loadable-ssr-addon-v5-slorber": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", @@ -15859,6 +15811,7 @@ }, "@kiwicopple/prism-react-renderer": { "version": "git+ssh://git@github.com/kiwicopple/prism-react-renderer.git#4a09100a587bce2d94d7ac8ed3564a61c6e70781", + "integrity": "sha512-QluokmJkMoHOwahIBN34hTG5n03kFyJ0REDq4mkf0o05JUC61ikXQsPNXtynASqoZRW7A+bKVNT65kCtUfgZlQ==", "from": "@kiwicopple/prism-react-renderer@https://github.com/kiwicopple/prism-react-renderer.git", "requires": { "prismjs": "^1.22.0" @@ -20417,16 +20370,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lodash.assignin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" - }, - "lodash.bind": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" - }, "lodash.curry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", @@ -20437,66 +20380,16 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "lodash.filter": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, "lodash.flow": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=" }, - "lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" - }, - "lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" - }, - "lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" - }, - "lodash.reject": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" - }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" - }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -21880,6 +21773,14 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-loadable": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-loadable/-/react-loadable-5.5.0.tgz", + "integrity": "sha512-C8Aui0ZpMd4KokxRdVAm2bQtI03k2RMRNzOB+IipV3yxFTSVICv7WoUr5L9ALB5BmKO1iHgZtWM8EvYG83otdg==", + "requires": { + "prop-types": "^15.5.0" + } + }, "react-loadable-ssr-addon-v5-slorber": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", diff --git a/web/package.json b/web/package.json index 72777af1c46..c884e546384 100644 --- a/web/package.json +++ b/web/package.json @@ -24,7 +24,8 @@ "mermaid": "^8.13.8", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-inlinesvg": "^2.0.1" + "react-inlinesvg": "^2.0.1", + "react-loadable": "^5.5.0" }, "browserslist": { "production": [ diff --git a/web/spec/combined.json b/web/spec/combined.json index 02d926a7d22..6b69c47f2c7 100644 --- a/web/spec/combined.json +++ b/web/spec/combined.json @@ -63359,7 +63359,7 @@ "isExported": true }, "comment": { - "text": "A unique identifier for the bucket you are creating.\n" + "text": "A unique identifier for the bucket you are updating.\n" }, "type": { "type": "intrinsic", @@ -64901,7 +64901,7 @@ "isExported": true }, "comment": { - "text": "A unique identifier for the bucket you are creating.\n" + "text": "A unique identifier for the bucket you are updating.\n" }, "type": { "type": "intrinsic", diff --git a/web/spec/storage.json b/web/spec/storage.json index 82c0be5f743..596f8be4763 100644 --- a/web/spec/storage.json +++ b/web/spec/storage.json @@ -1267,7 +1267,7 @@ "isExported": true }, "comment": { - "text": "A unique identifier for the bucket you are creating.\n" + "text": "A unique identifier for the bucket you are updating.\n" }, "type": { "type": "intrinsic", @@ -2809,7 +2809,7 @@ "isExported": true }, "comment": { - "text": "A unique identifier for the bucket you are creating.\n" + "text": "A unique identifier for the bucket you are updating.\n" }, "type": { "type": "intrinsic", diff --git a/web/spec/supabase.yml b/web/spec/supabase.yml index 68aeda4ec02..5dfdaebf6db 100644 --- a/web/spec/supabase.yml +++ b/web/spec/supabase.yml @@ -1449,7 +1449,7 @@ pages: - name: Upload file using `ArrayBuffer` from base64 file data js: | ```js - import {decode} from 'base64-arraybuffer' + import { decode } from 'base64-arraybuffer' const { data, error } = await supabase .storage diff --git a/web/src/css/custom.css b/web/src/css/custom.css index c212c163e29..8b10c9e57eb 100755 --- a/web/src/css/custom.css +++ b/web/src/css/custom.css @@ -1285,6 +1285,7 @@ h4.method-list-item-label { h4.method-list-item-label .method-list-item-label-name { font-family: var(--ifm-font-family-monospace); font-size: 0.8rem; + margin-right: 4px; padding: var(--ifm-code-padding-vertical) 0; } h4.method-list-item-label .method-list-item-label-badge { diff --git a/web/static/img/guides/integrations/plasmic/api-token-plasmic-05.png b/web/static/img/guides/integrations/plasmic/api-token-plasmic-05.png index df65b30ec5c..3a762a7db7d 100644 Binary files a/web/static/img/guides/integrations/plasmic/api-token-plasmic-05.png and b/web/static/img/guides/integrations/plasmic/api-token-plasmic-05.png differ diff --git a/web/static/img/guides/integrations/plasmic/application-screenshot-00.png b/web/static/img/guides/integrations/plasmic/application-screenshot-00.png index 92ca93b40c0..6d3b88b8757 100644 Binary files a/web/static/img/guides/integrations/plasmic/application-screenshot-00.png and b/web/static/img/guides/integrations/plasmic/application-screenshot-00.png differ diff --git a/web/static/img/guides/integrations/plasmic/application-screenshot-03.png b/web/static/img/guides/integrations/plasmic/application-screenshot-03.png index 888ddecd103..a54d9c361bd 100644 Binary files a/web/static/img/guides/integrations/plasmic/application-screenshot-03.png and b/web/static/img/guides/integrations/plasmic/application-screenshot-03.png differ diff --git a/web/static/img/guides/integrations/plasmic/clone-project-plasmic-04.png b/web/static/img/guides/integrations/plasmic/clone-project-plasmic-04.png index df2f66a76ac..02b8351206e 100644 Binary files a/web/static/img/guides/integrations/plasmic/clone-project-plasmic-04.png and b/web/static/img/guides/integrations/plasmic/clone-project-plasmic-04.png differ diff --git a/web/static/img/supabase-oauth-logins.png b/web/static/img/supabase-oauth-logins.png index 2e2aa3db2be..29c000c4550 100644 Binary files a/web/static/img/supabase-oauth-logins.png and b/web/static/img/supabase-oauth-logins.png differ