From 6a05a72a33f0e1d3e78e84901a1fa144f5a5cc02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20Nu=C3=B1ez=20Dosio?= Date: Wed, 5 Oct 2022 21:52:09 +0100 Subject: [PATCH 1/2] Updated anchor --- .../_blog/2022-10-05-supabase-beta-update-september-2022.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/_blog/2022-10-05-supabase-beta-update-september-2022.mdx b/apps/www/_blog/2022-10-05-supabase-beta-update-september-2022.mdx index 0c838e9d652..bcadf863f61 100644 --- a/apps/www/_blog/2022-10-05-supabase-beta-update-september-2022.mdx +++ b/apps/www/_blog/2022-10-05-supabase-beta-update-september-2022.mdx @@ -53,7 +53,7 @@ Visit the repo: [Snaplet](https://github.com/snaplet/postgres-wasm) | [Supaba We're making some changes to the way the Dashboard interacts with your database. These changes simplify the database permissions so that it's easier for you to migrate in and out of Supabase, and they reduce the security surface area considerably. The change will be applied automatically in November, or you can run it today via the Dashboard. -[See the PR](https://github.com/supabase/supabase/discussions/9314). +[Review the Security Notice](https://github.com/supabase/supabase/discussions/9314). ## Quick product updates From 20fc23ed21a6b3ea29644973e948eb4c5164c1c0 Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Thu, 6 Oct 2022 09:26:43 +0800 Subject: [PATCH 2/2] feat: add notification for schema migration (#9229) * feat: add notification for schema migration * chore: add finalise button to notification * Pull master, revert package-lock * chore: update spelling * chore: generalize confirm modal text * Update UI for notifications, specifically for the upcoming schema migration * Update messaging grammar * Update shared-types to 0.1.29 * Fix UI bugs * fix: use desired state for action button * chore: fix temp role and unmigrated order Co-authored-by: Joshen Lim --- package-lock.json | 16 +- .../Home/NewProjectPanel/GetStartedHero.tsx | 2 +- .../NotificationActions.tsx | 93 +++++++-- .../NotificationsPopover/NotificationRow.tsx | 42 +++- .../NotificationRows.utils.tsx | 104 ++++++++-- .../NotificationsPopover/index.tsx | 188 +++++++++++++++++- studio/package.json | 2 +- 7 files changed, 400 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c321780a98..4feedc576c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8283,9 +8283,9 @@ } }, "node_modules/@supabase/shared-types": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.28.tgz", - "integrity": "sha512-hwrvjr+a72gPfkvxN8Q/BClkPNtgUZ7VIbseypbhflRFh4SX4DJ3RKuk5shCtFGuMtk0XkjSxjhKi6VBLdbZ7Q==" + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.29.tgz", + "integrity": "sha512-CQEr/dKLNt3+du3xhpqohO3fkVFCLL+v92Vgj51cECpAv3LIkkxGKNNc+SXsG+niDxMZp9EuyDLo7fGzsB8f6w==" }, "node_modules/@supabase/storage-js": { "version": "2.0.0-rc.2", @@ -34203,7 +34203,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.25", + "@supabase/shared-types": "^0.1.29", "@supabase/supabase-js": "^2.0.0-rc.2", "@supabase/ui": "0.37.0-alpha.81", "ajv": "^8.6.3", @@ -40280,9 +40280,9 @@ } }, "@supabase/shared-types": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.28.tgz", - "integrity": "sha512-hwrvjr+a72gPfkvxN8Q/BClkPNtgUZ7VIbseypbhflRFh4SX4DJ3RKuk5shCtFGuMtk0XkjSxjhKi6VBLdbZ7Q==" + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.29.tgz", + "integrity": "sha512-CQEr/dKLNt3+du3xhpqohO3fkVFCLL+v92Vgj51cECpAv3LIkkxGKNNc+SXsG+niDxMZp9EuyDLo7fGzsB8f6w==" }, "@supabase/storage-js": { "version": "2.0.0-rc.2", @@ -57362,7 +57362,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.25", + "@supabase/shared-types": "^0.1.29", "@supabase/supabase-js": "^2.0.0-rc.2", "@supabase/ui": "0.37.0-alpha.81", "@tailwindcss/typography": "^0.5.2", diff --git a/studio/components/interfaces/Home/NewProjectPanel/GetStartedHero.tsx b/studio/components/interfaces/Home/NewProjectPanel/GetStartedHero.tsx index 7355de951cc..d4401fd7a21 100644 --- a/studio/components/interfaces/Home/NewProjectPanel/GetStartedHero.tsx +++ b/studio/components/interfaces/Home/NewProjectPanel/GetStartedHero.tsx @@ -57,7 +57,7 @@ const GetStartedHero = () => {
diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationActions.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationActions.tsx index 1bf5f150b46..ce7572803c8 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationActions.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationActions.tsx @@ -1,39 +1,102 @@ -import { FC } from 'react' +import { FC, Fragment } from 'react' import { useRouter } from 'next/router' -import { Button } from '@supabase/ui' +import { Button, IconExternalLink } from '@supabase/ui' +import { Action, ActionReason, ActionType } from '@supabase/shared-types/out/notifications' import { Project } from 'types' +import Link from 'next/link' + +// [Joshen TODO] Remove all things about "ownerReassignStatus" after 5th November +// double check with Qiao before we remove them. interface Props { project: Project - availableActions: any[] + changelogLink?: string + ownerReassignStatus?: any + availableActions: Action[] onSelectRestartProject: () => void + onSelectApplyMigration: () => void + onSelectRollbackMigration: () => void + onSelectFinalizeMigration: () => void } -const NotificationActions: FC = ({ project, availableActions, onSelectRestartProject }) => { +const NotificationActions: FC = ({ + project, + changelogLink, + ownerReassignStatus, + availableActions, + onSelectRestartProject, + onSelectApplyMigration, + onSelectRollbackMigration, + onSelectFinalizeMigration, +}) => { const router = useRouter() const onSelectUpgradeProject = () => { return router.push(`/project/${project.ref}/settings/billing/update/pro`) } - return ( -
- {availableActions.map((action: any) => { - if (action.action_type === 'project.upgrade') { + const renderActionButton = (action: Action) => { + switch (action.action_type) { + case ActionType.UpgradeProjectToPro: + return ( + + ) + case ActionType.SchedulePostgresRestart: + return ( + + ) + case ActionType.MigratePostgresSchema: + if (action.reason === ActionReason.Finalize) { return ( - + ownerReassignStatus?.desired !== 'migrated' && ( + + ) ) - } else if (action.action_type === 'postgresql.restart') { + } else if (action.reason === ActionReason.Rollback) { + return ( + ownerReassignStatus?.desired === 'temp_role' && ( + + ) + ) + } else { return ( - + ownerReassignStatus?.desired === 'unmigrated' && ( + + ) ) } + } + } + + return ( +
+ {availableActions.map((action) => { + return ( + + {renderActionButton(action)} + + ) })} + {changelogLink && ( + + + + + + )}
) } diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRow.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRow.tsx index e49459afc78..d9f170f626c 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRow.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRow.tsx @@ -1,4 +1,5 @@ import dayjs from 'dayjs' +import useSWR from 'swr' import { FC } from 'react' import { Notification, NotificationStatus } from '@supabase/shared-types/out/notifications' @@ -6,38 +7,65 @@ import { useStore } from 'hooks' import { Project } from 'types' import { formatNotificationCTAText, formatNotificationText } from './NotificationRows.utils' import NotificationActions from './NotificationActions' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' interface Props { notification: Notification onSelectRestartProject: (project: Project, notification: Notification) => void + onSelectApplyMigration: (project: Project, notification: Notification) => void + onSelectRollbackMigration: (project: Project, notification: Notification) => void + onSelectFinalizeMigration: (project: Project, notification: Notification) => void } -const NotificationRow: FC = ({ notification, onSelectRestartProject }) => { +const NotificationRow: FC = ({ + notification, + onSelectRestartProject, + onSelectApplyMigration, + onSelectRollbackMigration, + onSelectFinalizeMigration, +}) => { const { app } = useStore() const [project] = app.projects.list((project: Project) => project.id === notification.project_id) const insertedAt = dayjs(notification.inserted_at).format('DD MMM YYYY, HH:mma') + const changelogLink = (notification.data as any).changelog_link const availableActions = notification.meta?.actions_available ?? [] + // [Joshen TODO] This should be removed after 5th November when the migration notifications + // have been removed, double check with Qiao before removing. + // Relevant PR: https://github.com/supabase/supabase/pull/9229 + const { data: ownerReassignStatus } = useSWR( + (notification.data as any).upgrade_type === 'schema-migration' + ? `${API_URL}/database/${project.ref}/owner-reassign` + : null, + get + ) + return (
-
+
{notification.notification_status !== NotificationStatus.Seen && (
)}
-
-
- {formatNotificationText(project, notification)} -

{formatNotificationCTAText(availableActions)}

+
+
+ {formatNotificationText(project, notification, ownerReassignStatus)} + {formatNotificationCTAText(availableActions, ownerReassignStatus)}

{insertedAt}

-
+
{availableActions.length > 0 && ( onSelectRestartProject(project, notification)} + onSelectApplyMigration={() => onSelectApplyMigration(project, notification)} + onSelectRollbackMigration={() => onSelectRollbackMigration(project, notification)} + onSelectFinalizeMigration={() => onSelectFinalizeMigration(project, notification)} /> )}
diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.tsx index 97051471ffc..f272ea5619e 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.tsx @@ -1,6 +1,8 @@ +import dayjs from 'dayjs' import { Project } from 'types' import { Action, + ActionReason, ActionType, ExtensionsUpgrade, Notification, @@ -12,7 +14,11 @@ import { import { IconArrowRight, IconExternalLink } from '@supabase/ui' import Link from 'next/link' -export const formatNotificationText = (project: Project, notification: Notification) => { +export const formatNotificationText = ( + project: Project, + notification: Notification, + ownerReassignStatus?: any +) => { const projectName = project.name if (notification.data.name === NotificationName.ProjectExceedingTierLimit) { @@ -44,6 +50,22 @@ export const formatNotificationText = (project: Project, notification: Notificat New version of "{name}" ({version_to}) is now available for project "{projectName}".

) + } else if (upgrade_type === 'schema-migration') { + const { name, version_to } = additional as ExtensionsUpgrade + return ( +
+

A new schema migration is available for your project "{projectName}".

+
    +
  1. +
    +

    {name}

    + +

    {version_to}

    +
    +
  2. +
+
+ ) } return '' } else if (notification.data.name === NotificationName.PostgresqlUpgradeCompleted) { @@ -65,15 +87,33 @@ export const formatNotificationText = (project: Project, notification: Notificat {projectName}".

) + } else if (upgrade_type === 'schema-migration') { + const { version_to } = additional + if (ownerReassignStatus?.desired === 'unmigrated') { + return ( +

+ The schema migration "{version_to}" will be applied for project "{projectName}" within a + few days. You may opt to apply the changes now, or it'll be done so automatically. +

+ ) + } else if (ownerReassignStatus?.desired === 'temp_role') { + return ( +

+ The schema migration "{version_to}" will be finalized for project "{projectName}" within + a few days. You may opt to finalize the changes now, or it'll be done so automatically. +

+ ) + } else { + return ( +

+ The schema migration "{version_to}" has been successfully applied for project " + {projectName}". +

+ ) + } } } else if (notification.data.name === NotificationName.ProjectUpdateCompleted) { const { upgrades } = notification.data - const upgradesText = upgrades - .map( - (upgrade: ServiceUpgrade) => - `${upgrade.name}: ${upgrade.version_to} ${upgrade.changelog_link}` - ) - .reduce((a: string, b: string) => `${a}\n${b}`) return (

@@ -117,17 +157,57 @@ export const formatNotificationText = (project: Project, notification: Notificat } } -export const formatNotificationCTAText = (availableActions: Action[]) => { +export const formatNotificationCTAText = ( + availableActions: Action[], + ownerReassignStatus?: any +) => { const [action] = availableActions - if (!action) return '' + if (!action) return

switch (action.action_type) { case ActionType.SchedulePostgresRestart: - return 'Restart your project to get the latest updates.' + return

Restart your project to get the latest updates.

case ActionType.UpgradeProjectToPro: - return 'Upgrade your project to ensure continued availability.' + return

Upgrade your project to ensure continued availability.

case ActionType.PgBouncerRestart: - return 'Restart your connection pooler to get the latest updates.' + return

Restart your connection pooler to get the latest updates.

+ case ActionType.MigratePostgresSchema: + if (action.deadline) { + if (ownerReassignStatus?.desired === 'migrated') { + return ( +

+ This patch was applied on{' '} + {dayjs(new Date(ownerReassignStatus.migrated_at ?? action.deadline)).format('DD MMM YYYY, HH:mma')} +

+ ) + } else if (ownerReassignStatus?.desired === 'temp_role') { + if (action.reason === ActionReason.Finalize) { + return ( +

+ This patch will be automatically applied after{' '} + {dayjs(new Date(action.deadline)).format('DD MMM YYYY, HH:mma')} +

+ ) + } else { + return ( +

+ This patch was applied on{' '} + {dayjs(new Date(ownerReassignStatus.modified_at)).format('DD MMM YYYY, HH:mma')} +

+ ) + } + } else { + return ( +

+ This patch will be automatically applied after{' '} + {dayjs(new Date(action.deadline)).format('DD MMM YYYY, HH:mma')} +

+ ) + } + } else { + return '' + } + default: return '' } diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx index 198498f16ab..a815df47857 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx @@ -1,6 +1,7 @@ +import dayjs from 'dayjs' import { FC, Fragment, useState } from 'react' import { useRouter } from 'next/router' -import { Button, IconBell, Popover } from '@supabase/ui' +import { Button, IconBell, Popover, IconArrowRight, Alert } from '@supabase/ui' import { Notification, NotificationStatus, @@ -9,7 +10,7 @@ import { import { Project } from 'types' import { useNotifications, useStore } from 'hooks' -import { patch, post } from 'lib/common/fetch' +import { delete_, patch, post } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import NotificationRow from './NotificationRow' import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' @@ -22,6 +23,10 @@ const NotificationsPopover: FC = () => { const { notifications, refresh } = useNotifications() const [projectToRestart, setProjectToRestart] = useState() + const [projectToApplyMigration, setProjectToApplyMigration] = useState() + const [projectToRollbackMigration, setProjectToRollbackMigration] = useState() + const [projectToFinalizeMigration, setProjectToFinalizeMigration] = useState() + const [targetNotification, setTargetNotification] = useState() if (!notifications) return <> @@ -53,6 +58,7 @@ const NotificationsPopover: FC = () => { const serviceNamesByActionName: Record = { [ActionType.PgBouncerRestart]: 'pgbouncer', [ActionType.SchedulePostgresRestart]: 'postgresql', + [ActionType.MigratePostgresSchema]: 'postgresql', } const services: string[] = targetNotification.meta.actions_available .map((action) => action.action_type) @@ -82,6 +88,76 @@ const NotificationsPopover: FC = () => { setTargetNotification(undefined) } + // [Joshen/Qiao] These are all very specific to the upcoming security patch + // https://github.com/supabase/supabase/discussions/9314 + // We probably need to revisit this again when we're planning to push out the next wave of + // notifications. Ideally, we should allow these to be more flexible and configurable + // Perhaps the URLs could come from the notification themselves if the actions + // require an external API call, then we just need one method instead of individual ones like this + + const onConfirmProjectApplyMigration = async () => { + if (!projectToApplyMigration) return + const res = await post(`${API_URL}/database/${projectToApplyMigration.ref}/owner-reassign`, {}) + if (!res.error) { + app.projects.fetchDetail(projectToApplyMigration.ref) + ui.setNotification({ + category: 'success', + message: `Succesfully applied migration for project "${projectToApplyMigration.name}"`, + }) + } else { + ui.setNotification({ + error: res.error, + category: 'error', + message: `Failed to apply migration: ${res.error.message}`, + }) + } + setProjectToApplyMigration(undefined) + } + + const onConfirmProjectRollbackMigration = async () => { + if (!projectToRollbackMigration) return + const res = await delete_( + `${API_URL}/database/${projectToRollbackMigration.ref}/owner-reassign`, + {} + ) + if (!res.error) { + app.projects.fetchDetail(projectToRollbackMigration.ref) + ui.setNotification({ + category: 'success', + message: `Succesfully rolled back migration for project "${projectToRollbackMigration.name}"`, + }) + } else { + ui.setNotification({ + error: res.error, + category: 'error', + message: `Failed to roll back migration: ${res.error.message}`, + }) + } + setProjectToRollbackMigration(undefined) + } + + const onConfirmProjectFinalizeMigration = async () => { + if (!projectToFinalizeMigration) return + const res = await patch( + `${API_URL}/database/${projectToFinalizeMigration.ref}/owner-reassign`, + {} + ) + if (!res.error) { + app.projects.fetchDetail(projectToFinalizeMigration.ref) + ui.setNotification({ + category: 'success', + message: `Succesfully finalized migration for project "${projectToFinalizeMigration.name}"`, + }) + } else { + ui.setNotification({ + error: res.error, + category: 'error', + message: `Failed to finalize migration: ${res.error.message}`, + }) + } + setProjectToFinalizeMigration(undefined) + } + return ( <> = () => { sideOffset={8} onOpenChange={onOpenChange} overlay={ -
+

Notifications

{/* Area for improvement: Paginate notifications and show in a side panel */} @@ -115,6 +191,18 @@ const NotificationsPopover: FC = () => { setProjectToRestart(project) setTargetNotification(notification) }} + onSelectApplyMigration={(project, notification) => { + setProjectToApplyMigration(project) + setTargetNotification(notification) + }} + onSelectRollbackMigration={(project, notification) => { + setProjectToRollbackMigration(project) + setTargetNotification(notification) + }} + onSelectFinalizeMigration={(project, notification) => { + setProjectToFinalizeMigration(project) + setTargetNotification(notification) + }} /> {i !== notifications.length - 1 && } @@ -151,6 +239,100 @@ const NotificationsPopover: FC = () => { onSelectCancel={() => setProjectToRestart(undefined)} onSelectConfirm={onConfirmProjectRestart} /> + +
+

The following schema migration will be applied to the project

+
    +
  1. +
    +

    {(targetNotification?.data as any)?.additional?.name}

    + +

    {(targetNotification?.data as any)?.additional?.version_to}

    +
    +
  2. +
+
+

+ This change can be rolled back anytime up till{' '} + {dayjs( + new Date(targetNotification?.meta.actions_available?.[0]?.deadline ?? 0) + ).format('DD MMM YYYY, HH:mma ZZ')} + , after which the changes will be finalized and can no longer be undone. +

+
+ } + buttonLabel="Confirm" + buttonLoadingLabel="Confirm" + onSelectCancel={() => setProjectToApplyMigration(undefined)} + onSelectConfirm={onConfirmProjectApplyMigration} + /> + +
+

The following schema migration will be rolled back for the project

+
    +
  1. +
    +

    {(targetNotification?.data as any)?.additional?.name}

    + +

    {(targetNotification?.data as any)?.additional?.version_to}

    +
    +
  2. +
+
+

+ This migration however will still be applied and finalized after{' '} + {dayjs( + new Date(targetNotification?.meta.actions_available?.[0]?.deadline ?? 0) + ).format('DD MMM YYYY, HH:mma ZZ')} + , after which the changes can no longer be undone. +

+
+ } + buttonLabel="Confirm" + buttonLoadingLabel="Confirm" + onSelectCancel={() => setProjectToRollbackMigration(undefined)} + onSelectConfirm={onConfirmProjectRollbackMigration} + /> + + +
+

The following schema migration will be finalized for the project

+
    +
  1. +
    +

    {(targetNotification?.data as any)?.additional?.name}

    + +

    {(targetNotification?.data as any)?.additional?.version_to}

    +
    +
  2. +
+
+
+ } + buttonLabel="Confirm" + buttonLoadingLabel="Confirm" + onSelectCancel={() => setProjectToFinalizeMigration(undefined)} + onSelectConfirm={onConfirmProjectFinalizeMigration} + /> ) } diff --git a/studio/package.json b/studio/package.json index a9c4f159a40..b0861231f2e 100644 --- a/studio/package.json +++ b/studio/package.json @@ -27,7 +27,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.25", + "@supabase/shared-types": "^0.1.29", "@supabase/supabase-js": "^2.0.0-rc.2", "@supabase/ui": "0.37.0-alpha.81", "ajv": "^8.6.3",