From 2f15159fcaa2f6b9ced3d3f2f6f57ff4239f8623 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 18 Jul 2022 20:34:37 +0800 Subject: [PATCH] Revert "Revert "chore: remove global loading spinner"" This reverts commit c5be38e52ae9a9a64b75193157ace501049e648d. --- .../App/GoTrueWrapper/GoTrueWrapper.tsx | 53 --- .../App/GoTrueWrapper/GoTrueWrapper.utils.ts | 7 - studio/components/interfaces/App/index.ts | 3 +- .../AuthProvidersForm.types.ts | 7 +- .../Auth/AuthProvidersForm/FormField.tsx | 137 +++++++ .../Auth/AuthProvidersForm/ProviderForm.tsx | 147 +------ .../Auth/EmailTemplates/TemplateEditor.tsx | 90 ++--- .../interfaces/Auth/SmtpForm/SmtpForm.tsx | 15 +- .../interfaces/Functions/CommandRender.tsx | 4 +- .../Organization/DeleteOrganizationButton.tsx | 123 ++++++ .../Organization/Organization.utils.ts | 9 + .../interfaces/Organization/OwnerDropdown.tsx | 202 ++++++++++ .../interfaces/Organization/index.ts | 11 +- .../layouts/AccountLayout/AccountLayout.tsx | 77 ++-- .../AccountLayout/AccountLayout.types.ts | 15 + .../layouts/AccountLayout/SidebarItem.tsx | 73 ++++ .../layouts/AccountLayout/WithSidebar.tsx | 104 ++--- studio/components/layouts/BillingLayout.tsx | 4 +- .../to-be-cleaned/Docs/Description.js | 4 +- studio/hooks/misc/withAuth.tsx | 12 +- studio/lib/common/fetch/base.ts | 27 +- studio/lib/common/fetch/delete.ts | 2 +- studio/lib/common/fetch/get.ts | 4 +- studio/lib/common/fetch/head.ts | 4 +- studio/lib/common/fetch/patch.ts | 2 +- studio/lib/common/fetch/post.ts | 2 +- studio/lib/gotrue.ts | 2 +- .../storageExplorer/StorageExplorerStore.js | 6 +- studio/package-lock.json | 120 +++--- studio/package.json | 2 +- studio/pages/_app.tsx | 19 +- studio/pages/index.tsx | 11 +- studio/pages/org/[slug]/settings.tsx | 367 ++---------------- studio/pages/project/[ref]/editor/[id].tsx | 4 +- studio/pages/project/[ref]/settings/api.tsx | 12 +- .../AuthProvidersFormValidation.tsx | 18 + .../components/Auth/Auth.constants.test.js | 12 + 37 files changed, 876 insertions(+), 835 deletions(-) delete mode 100644 studio/components/interfaces/App/GoTrueWrapper/GoTrueWrapper.tsx delete mode 100644 studio/components/interfaces/App/GoTrueWrapper/GoTrueWrapper.utils.ts create mode 100644 studio/components/interfaces/Auth/AuthProvidersForm/FormField.tsx create mode 100644 studio/components/interfaces/Organization/DeleteOrganizationButton.tsx create mode 100644 studio/components/interfaces/Organization/Organization.utils.ts create mode 100644 studio/components/interfaces/Organization/OwnerDropdown.tsx create mode 100644 studio/components/layouts/AccountLayout/AccountLayout.types.ts create mode 100644 studio/components/layouts/AccountLayout/SidebarItem.tsx diff --git a/studio/components/interfaces/App/GoTrueWrapper/GoTrueWrapper.tsx b/studio/components/interfaces/App/GoTrueWrapper/GoTrueWrapper.tsx deleted file mode 100644 index ceb818e004f..00000000000 --- a/studio/components/interfaces/App/GoTrueWrapper/GoTrueWrapper.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { FC, useEffect, useState } from 'react' -import { Subscription } from '@supabase/gotrue-js' - -import { auth } from 'lib/gotrue' -import Connecting from 'components/ui/Loading' -import { doesTokenDataExist } from './GoTrueWrapper.utils' - -/** - * On app first load, gotrue client may take a while to refresh access token - * We have to wait for that process to complete before showing children components - */ -const GoTrueWrapper: FC = ({ children }) => { - const [loading, setLoading] = useState(doesTokenDataExist()) - - useEffect(() => { - let subscription: Subscription | null - let timer: any - const currentSession = auth.session() - - function tokenRefreshed() { - setLoading(false) - // clean subscription - if (subscription) subscription.unsubscribe() - // clean timer - if (timer) clearTimeout(timer) - } - - if (currentSession != undefined && currentSession != null) { - // if there is an active session, go ahead - setLoading(false) - } else { - // else wait for TOKEN_REFRESHED event before continue - const response = auth.onAuthStateChange((_event, session) => { - if (loading && _event === 'TOKEN_REFRESHED') { - tokenRefreshed() - } - }) - subscription = response.data ?? null - - // we need a timeout here, in case token refresh fails - timer = setTimeout(() => setLoading(false), 5 * 1000) - } - - return () => { - if (subscription) subscription.unsubscribe() - if (timer) clearTimeout(timer) - } - }, []) - - return <>{loading ? : children} -} - -export default GoTrueWrapper diff --git a/studio/components/interfaces/App/GoTrueWrapper/GoTrueWrapper.utils.ts b/studio/components/interfaces/App/GoTrueWrapper/GoTrueWrapper.utils.ts deleted file mode 100644 index 6b3c03b60fe..00000000000 --- a/studio/components/interfaces/App/GoTrueWrapper/GoTrueWrapper.utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function doesTokenDataExist() { - // ignore if server-side - if (typeof window === 'undefined') return false - // check tokenData on localstorage - const tokenData = window?.localStorage['supabase.auth.token'] - return tokenData != undefined && typeof tokenData === 'string' -} diff --git a/studio/components/interfaces/App/index.ts b/studio/components/interfaces/App/index.ts index 4e92356a261..593df54f0f6 100644 --- a/studio/components/interfaces/App/index.ts +++ b/studio/components/interfaces/App/index.ts @@ -1,6 +1,5 @@ import PortalToast from './PortalToast' -import GoTrueWrapper from './GoTrueWrapper/GoTrueWrapper' import RouteValidationWrapper from './RouteValidationWrapper' import AppBannerWrapper from './AppBannerWrapper' -export { PortalToast, GoTrueWrapper, RouteValidationWrapper, AppBannerWrapper } +export { PortalToast, RouteValidationWrapper, AppBannerWrapper } diff --git a/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.types.ts b/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.types.ts index 1aef61b6539..fcc638cf630 100644 --- a/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.types.ts +++ b/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.types.ts @@ -12,14 +12,15 @@ export interface Provider { [x: string]: { title: string type: 'boolean' | 'string' | 'select' | 'number' - description?: string - descriptionOptional?: string - units?: string enum: Enum[] show: { key: string matches: string } + description?: string + descriptionOptional?: string + units?: string + isSecret?: boolean } } validationSchema: any // todo: use Yup type diff --git a/studio/components/interfaces/Auth/AuthProvidersForm/FormField.tsx b/studio/components/interfaces/Auth/AuthProvidersForm/FormField.tsx new file mode 100644 index 00000000000..d5abb12219d --- /dev/null +++ b/studio/components/interfaces/Auth/AuthProvidersForm/FormField.tsx @@ -0,0 +1,137 @@ +import { FC, useState } from 'react' +import ReactMarkdown from 'react-markdown' +import { Button, Input, InputNumber, Toggle, Listbox, IconEye, IconEyeOff } from '@supabase/ui' +import { Enum } from './AuthProvidersForm.types' + +interface Props { + name: string + properties: any + formValues: any +} + +const FormField: FC = ({ name, properties, formValues }) => { + if (properties.show && formValues[properties.show.key] !== properties.show.matches) return <> + + const [hidden, setHidden] = useState(!!properties.isSecret) + + switch (properties.type) { + case 'string': + return ( + + {properties.descriptionOptional} + + ) : null + } + descriptionText={ + properties.description ? ( + + {properties.description} + + ) : null + } + actions={ + !!properties.isSecret ? ( + + + setIsOpen(false)} + header={ +
+
Delete organisation
+ Are you sure? +
+ } + > +
+ {({ isSubmitting }: { isSubmitting: boolean }) => ( +
+ +

+ This action cannot be undone. This will + permanently delete the {orgName}{' '} + organization and remove all of its projects. +

+
+ + + + Please type {orgSlug} to confirm + + } + onChange={(e) => setValue(e.target.value)} + value={value} + placeholder="Type in the orgnaization name" + className="w-full" + /> + + + + + +
+ )} +
+
+ + ) +}) + +export default DeleteOrganizationButton diff --git a/studio/components/interfaces/Organization/Organization.utils.ts b/studio/components/interfaces/Organization/Organization.utils.ts new file mode 100644 index 00000000000..c2e815a905b --- /dev/null +++ b/studio/components/interfaces/Organization/Organization.utils.ts @@ -0,0 +1,9 @@ +// Invite is expired if older than 24hrs +export function isInviteExpired(timestamp: Date) { + const inviteDate = new Date(timestamp) + const now = new Date() + var timeBetween = now.valueOf() - inviteDate.valueOf() + if (timeBetween / 1000 / 60 / 60 < 24) { + return true + } +} diff --git a/studio/components/interfaces/Organization/OwnerDropdown.tsx b/studio/components/interfaces/Organization/OwnerDropdown.tsx new file mode 100644 index 00000000000..a8365eeb23a --- /dev/null +++ b/studio/components/interfaces/Organization/OwnerDropdown.tsx @@ -0,0 +1,202 @@ +import { useContext, useState } from 'react' +import { observer } from 'mobx-react-lite' +import { timeout } from 'lib/helpers' +import { Button, IconMoreHorizontal, IconTrash, Dropdown } from '@supabase/ui' + +import { useOrganizationDetail, useStore } from 'hooks' +import { Member } from 'types' +import { API_URL } from 'lib/constants' +import { post, delete_ } from 'lib/common/fetch' +import { isInviteExpired } from './Organization.utils' +import { PageContext } from 'pages/org/[slug]/settings' +import TextConfirmModal from 'components/ui/Modals/TextConfirmModal' +import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' + +const OwnerDropdown = observer(({ members, member }: any) => { + const { ui } = useStore() + const { mutateOrgMembers } = useOrganizationDetail(ui.selectedOrganization?.slug || '') + + const PageState: any = useContext(PageContext) + const [loading, setLoading] = useState(false) + + // handle modal visibility + const [ownerTransferIsVisble, setOwnerTransferIsVisble] = useState(false) + + const { id: orgId, slug: orgSlug, name: orgName } = PageState.organization + + async function handleMemberDelete() { + await timeout(200) + + confirmAlert({ + title: 'Confirm to remove', + message: `This is permanent! Are you sure you want to remove ${member.profile.primary_email}?`, + onAsyncConfirm: async () => { + setLoading(true) + const response = await delete_(`${API_URL}/organizations/${orgSlug}/members/remove`, { + member_id: member.id, + }) + if (response.error) { + ui.setNotification({ + category: 'error', + message: `Failed to delete user: ${response.error.message}`, + }) + setLoading(false) + } else { + const updatedMembers = members.filter((x: any) => x.id !== member.id) + mutateOrgMembers(updatedMembers) + ui.setNotification({ category: 'success', message: 'Successfully removed member' }) + } + }, + }) + } + + async function handleTransfer() { + setLoading(true) + + const response = await post(`${API_URL}/organizations/${orgSlug}/transfer`, { + org_id: orgId, + member_id: member.id, + }) + if (response.error) { + ui.setNotification({ + category: 'error', + message: `Failed to transfer ownership: ${response.error.message}`, + }) + setLoading(false) + } else { + const updatedMembers = [...members] + const oldOwner = updatedMembers.find((x) => x.is_owner == true) + if (oldOwner) oldOwner.is_owner = false + const newOwner = updatedMembers.find((x) => x.id == member.id) + if (newOwner) newOwner.is_owner = true + mutateOrgMembers(updatedMembers) + setOwnerTransferIsVisble(false) + ui.setNotification({ category: 'success', message: 'Successfully transfered organization' }) + } + } + + async function handleResendInvite(member: Member) { + setLoading(true) + + const response = await post(`${API_URL}/organizations/${orgSlug}/members/invite`, { + invited_email: member.profile.primary_email, + owner_id: member.invited_id, + }) + + if (response.error) { + ui.setNotification({ + category: 'error', + message: `Failed to resend invitation: ${response.error.message}`, + }) + setLoading(false) + } else { + const updatedMembers = [...members] + mutateOrgMembers(updatedMembers) + ui.setNotification({ category: 'success', message: 'Resent the invitation.' }) + setLoading(false) + } + } + + async function handleRevokeInvitation(id: number) { + setLoading(true) + + const response = await delete_( + `${API_URL}/organizations/${orgSlug}/members/invite?invited_id=${id}`, + {} + ) + + if (response.error) { + ui.setNotification({ + category: 'error', + message: `Failed to revoke invitation: ${response.error.message}`, + }) + setLoading(false) + } else { + const updatedMembers = [...members] + mutateOrgMembers(updatedMembers) + ui.setNotification({ category: 'success', message: 'Successfully revoked the invitation.' }) + } + } + + return ( +
+ + {!member.invited_at && ( + setOwnerTransferIsVisble(!ownerTransferIsVisble)}> +
+

Make owner

+

Transfer ownership of "{orgName}"

+
+
+ )} + + {member.invited_at && ( + <> + handleRevokeInvitation(member.invited_id)}> +
+

Cancel invitation

+

Revoke this invitation.

+
+
+ + {!isInviteExpired(member.invited_at) && ( + <> + + handleResendInvite(member)}> +
+

Resend invitation

+

Invites expire after 24hrs.

+
+
+ + )} + + )} + + {!member.invited_at && ( + <> + + } onClick={handleMemberDelete}> + Remove member + + + )} + + } + > + +
+ + setOwnerTransferIsVisble(!ownerTransferIsVisble)} + onConfirm={handleTransfer} + alert="Payment methods such as credit cards will also be transferred. You may want to delete credit card information first before transferring." + text={ + + By transferring this organization, it will be solely owned by{' '} + {member.profile?.username}, they + will also be able to remove you from the organization as a member + + } + /> +
+ ) +}) + +export default OwnerDropdown diff --git a/studio/components/interfaces/Organization/index.ts b/studio/components/interfaces/Organization/index.ts index 97ce7937b61..683ac5fda3c 100644 --- a/studio/components/interfaces/Organization/index.ts +++ b/studio/components/interfaces/Organization/index.ts @@ -1,4 +1,13 @@ import BillingSettings from './BillingSettings/BillingSettings' import InvoicesSettings from './InvoicesSettings/InvoicesSettings' +import InviteMemberModal from './InviteMemberModal' +import DeleteOrganizationButton from './DeleteOrganizationButton' +import OwnerDropdown from './OwnerDropdown' -export { BillingSettings, InvoicesSettings } +export { + BillingSettings, + InvoicesSettings, + InviteMemberModal, + DeleteOrganizationButton, + OwnerDropdown, +} diff --git a/studio/components/layouts/AccountLayout/AccountLayout.tsx b/studio/components/layouts/AccountLayout/AccountLayout.tsx index dd57fa311fd..f25924a6652 100644 --- a/studio/components/layouts/AccountLayout/AccountLayout.tsx +++ b/studio/components/layouts/AccountLayout/AccountLayout.tsx @@ -1,21 +1,22 @@ import Head from 'next/head' +import { FC, ReactNode } from 'react' import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' -import { API_URL, IS_PLATFORM } from 'lib/constants' +import { IS_PLATFORM } from 'lib/constants' import { useStore, withAuth, useFlag } from 'hooks' import WithSidebar from './WithSidebar' import { auth } from 'lib/gotrue' -/** - * layout for dashboard homepage, account and org settings - * - * @param {String} title - * @param {JSX.Element|JSX.Element[]} children - * @param {Array} breadcrumbs - */ +// Layout for dashboard homepage, account and org settings -const AccountLayout = ({ children, title, breadcrumbs }: any) => { +interface Props { + title: string + breadcrumbs: Object[] + children: ReactNode +} + +const AccountLayout: FC = ({ children, title, breadcrumbs }) => { const router = useRouter() const { app, ui } = useStore() @@ -27,36 +28,29 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => { router.reload() } - const baseLogoutLink = { - icon: '/icons/feather/power.svg', - label: 'Logout', - href: `${API_URL}/logout`, - key: `${API_URL}/logout`, - } - - const logoutLink = { ...baseLogoutLink, href: undefined, onClick: onClickLogout } - const organizationsLinks = app.organizations .list() .map((x: any) => ({ - isActive: router.pathname.startsWith('/org/') && ui.selectedOrganization?.slug == x.slug, - label: x.name, + key: `${x.slug}-settings`, href: `/org/${x.slug}/settings`, + label: x.name, + isActive: router.pathname.startsWith('/org/') && ui.selectedOrganization?.slug === x.slug, })) .sort((a, b) => a.label.localeCompare(b.label)) - let linksWithHeaders = [ + const sidebarLinks = [ { heading: 'Projects', links: [ { - isActive: router.pathname == '/', - label: 'All projects', + key: 'all-projects', href: '/', + label: 'All projects', + isActive: router.pathname === '/', }, ], }, - ...(IS_PLATFORM + ...(IS_PLATFORM && organizationsLinks.length > 0 ? [ { heading: 'Organizations', @@ -70,18 +64,18 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => { heading: 'Account', links: [ { - isActive: router.pathname == `/account/me`, + key: 'account-me', + href: '/account/me', icon: '/img/user.svg', label: 'Preferences', - href: `/account/me`, - key: `/account/me`, + isActive: router.pathname === '/account/me', }, { - isActive: router.pathname == `/account/tokens`, + key: 'account-tokens', + href: '/account/tokens', icon: '/img/user.svg', label: 'Access Tokens', - href: `/account/tokens`, - key: `/account/tokens`, + isActive: router.pathname === '/account/tokens', }, ], }, @@ -92,26 +86,33 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => { links: [ { key: 'ext-guides', + href: 'https://supabase.com/docs', icon: '/img/book.svg', label: 'Guides', - href: 'https://supabase.com/docs', - external: true, + isExternal: true, }, { key: 'ext-guides', + href: 'https://supabase.com/docs/guides/api', icon: '/img/book-open.svg', label: 'API Reference', - href: 'https://supabase.com/docs/guides/api', - external: true, + isExternal: true, }, ], }, { - links: [logoutLink], + heading: '', + links: [ + { + key: 'logout', + icon: '/icons/feather/power.svg', + label: 'Logout', + onClick: onClickLogout, + }, + ], }, ] - if (!organizationsLinks?.length) - linksWithHeaders = linksWithHeaders.filter((x: any) => x.heading != 'Organizations') + return ( <> @@ -124,7 +125,7 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => { style={{ height: maxHeight, maxHeight }} className="flex w-full flex-1 flex-col overflow-y-auto" > - + {children} diff --git a/studio/components/layouts/AccountLayout/AccountLayout.types.ts b/studio/components/layouts/AccountLayout/AccountLayout.types.ts new file mode 100644 index 00000000000..89144025c95 --- /dev/null +++ b/studio/components/layouts/AccountLayout/AccountLayout.types.ts @@ -0,0 +1,15 @@ +export interface SidebarLinkGroup { + heading: string + links: SidebarLink[] +} + +export interface SidebarLink { + key: string + label: string + icon?: string + href?: string + subitemsKey?: string // I dont think this is being used at all + isActive?: boolean + isExternal?: boolean + onClick?: () => void +} diff --git a/studio/components/layouts/AccountLayout/SidebarItem.tsx b/studio/components/layouts/AccountLayout/SidebarItem.tsx new file mode 100644 index 00000000000..a5d63c3b125 --- /dev/null +++ b/studio/components/layouts/AccountLayout/SidebarItem.tsx @@ -0,0 +1,73 @@ +import { FC } from 'react' +import Link from 'next/link' +import { Menu, IconLogOut, IconArrowUpRight } from '@supabase/ui' + +interface Props { + id: any + label: string + href?: string + isActive?: boolean + isSubitem?: boolean + isExternal?: boolean + onClick?: () => void +} + +const SidebarItem: FC = ({ + id, + label, + href, + isActive = false, + isSubitem = false, + isExternal = false, + onClick = () => {}, +}) => { + if (href === undefined) { + const icon = isExternal ? ( + + ) : label === 'Logout' ? ( + + ) : undefined + + return ( + + {isSubitem ?

{label}

: label} +
+ ) + } + + return ( + + + + + + ) +} + +export default SidebarItem diff --git a/studio/components/layouts/AccountLayout/WithSidebar.tsx b/studio/components/layouts/AccountLayout/WithSidebar.tsx index 4b30bd47054..036844fdbb2 100644 --- a/studio/components/layouts/AccountLayout/WithSidebar.tsx +++ b/studio/components/layouts/AccountLayout/WithSidebar.tsx @@ -1,14 +1,15 @@ -import Link from 'next/link' import { FC, ReactNode } from 'react' -import { isUndefined } from 'lodash' -import { Menu, Typography, IconArrowUpRight, Badge, IconLogOut } from '@supabase/ui' +import { Menu, Badge } from '@supabase/ui' + import { useFlag } from 'hooks' import LayoutHeader from '../ProjectLayout/LayoutHeader' +import SidebarItem from './SidebarItem' +import { SidebarLink, SidebarLinkGroup } from './AccountLayout.types' interface Props { title: string breadcrumbs: any[] - links: any[] + links: SidebarLinkGroup[] | SidebarLink[] header?: ReactNode subitems?: any[] subitemsParentKey?: number @@ -29,7 +30,7 @@ const WithSidebar: FC = ({ customSidebarContent, }) => { const noContent = !links && !customSidebarContent - const linksHaveHeaders = links && links[0].heading + const linksHaveHeaders = links && (links[0] as any)?.heading const ongoingIncident = useFlag('ongoingIncident') const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh' @@ -46,28 +47,24 @@ const WithSidebar: FC = ({ ].join(' ')} > {title && ( -
-
- - {title} - -
+
+

{title}

)} - {header && header} + {header !== undefined && header}
{customSidebarContent} {links && linksHaveHeaders ? ( ) : null} {!linksHaveHeaders && links ? ( @@ -86,8 +83,8 @@ const WithSidebar: FC = ({ export default WithSidebar const LinksWithHeaders: FC = ({ links, subitems, subitemsParentKey }) => { - return links.map((x: any) => ( -
+ return links.map((x: any, i: number) => ( +
{x.heading && } {x.versionLabel && (
@@ -104,23 +101,26 @@ const LinksWithHeaders: FC = ({ links, subitems, subitemsParentKey }) => {
)) } -const LinksWithoutHeaders: FC = ({ links, subitems, subitemsParentKey }) => { + +const LinksWithoutHeaders: FC<{ + links: SidebarLink[] + subitems?: any[] + subitemsParentKey?: number +}> = ({ links, subitems, subitemsParentKey }) => { return (
    - {links.map((x: any, i: number) => { - // disable active state for link with subitems + {links.map((x: SidebarLink, i: number) => { + // Disable active state for link with subitems const isActive = x.isActive && !subitems - let render: any = ( ) @@ -129,11 +129,10 @@ const LinksWithoutHeaders: FC = ({ links, subitems, subitemsParentKey }) => )) render = [render, ...subItemsRender] @@ -144,54 +143,3 @@ const LinksWithoutHeaders: FC = ({ links, subitems, subitemsParentKey }) =>
) } - -const SidebarItem: FC = ({ id, label, href, isActive, isSubitem, onClick, external }) => { - if (isUndefined(href)) { - let icon - if (external) { - icon = - } - - if (label === 'Logout') { - icon = - } - - return ( - {})} - icon={icon} - > - {isSubitem ?

{label}

: label} -
- ) - } - - return ( - - - - - - ) -} diff --git a/studio/components/layouts/BillingLayout.tsx b/studio/components/layouts/BillingLayout.tsx index 14d830aac29..d82aeae034d 100644 --- a/studio/components/layouts/BillingLayout.tsx +++ b/studio/components/layouts/BillingLayout.tsx @@ -17,14 +17,14 @@ const BillingLayout: FC = ({ children }) => {
{/* Header */} -
+
-

Customize your plan

+

Customize your plan

{ui.selectedOrganization?.name}

diff --git a/studio/components/to-be-cleaned/Docs/Description.js b/studio/components/to-be-cleaned/Docs/Description.js index a6258425d14..e1e680434c4 100644 --- a/studio/components/to-be-cleaned/Docs/Description.js +++ b/studio/components/to-be-cleaned/Docs/Description.js @@ -44,8 +44,8 @@ export default function Description({ content, metadata, onChange = () => {} }) let query = '' let description = value.replaceAll("'", "''") if (table && column) - query = `comment on column public.${table}.${column} is '${description}';` - if (table && !column) query = `comment on table public.${table} is '${description}';` + query = `comment on column public."${table}"."${column}" is '${description}';` + if (table && !column) query = `comment on table public."${table}" is '${description}';` if (rpc) query = `comment on function ${rpc} is '${description}';` if (query) { diff --git a/studio/hooks/misc/withAuth.tsx b/studio/hooks/misc/withAuth.tsx index cb8a2640afd..6ed9a74c474 100644 --- a/studio/hooks/misc/withAuth.tsx +++ b/studio/hooks/misc/withAuth.tsx @@ -1,8 +1,7 @@ import { useProfile, useStore } from 'hooks' -import { ComponentType, useEffect, useState } from 'react' +import { ComponentType, useEffect } from 'react' import { NextRouter, useRouter } from 'next/router' import { IS_PLATFORM } from 'lib/constants' -import Connecting from 'components/ui/Loading' const PLATFORM_ONLY_PAGES = ['storage', 'reports', 'settings'] @@ -16,7 +15,6 @@ export function withAuth( return (props: any) => { const router = useRouter() const rootStore = useStore() - const [isConnecting, setConnecting] = useState(true) const { ref, slug } = router.query const { app, ui } = rootStore @@ -64,14 +62,6 @@ export function withAuth( } }, [isLoading, router.isReady, ref, slug]) - useEffect(() => { - if (!isLoading && !isRedirecting && router.isReady) { - setConnecting(false) - } - }, [isLoading, isRedirecting, router.isReady]) - - if (isConnecting) return - return } } diff --git a/studio/lib/common/fetch/base.ts b/studio/lib/common/fetch/base.ts index 69bab2587ba..b13b7a026b5 100644 --- a/studio/lib/common/fetch/base.ts +++ b/studio/lib/common/fetch/base.ts @@ -1,3 +1,4 @@ +import { auth } from 'lib/gotrue' import { tryParseJson } from 'lib/helpers' import { isUndefined } from 'lodash' import { SupaResponse } from 'types/base' @@ -83,22 +84,22 @@ export async function handleResponseError( } } -export function getAccessToken() { +export async function getAccessToken() { // ignore if server-side if (typeof window === 'undefined') return '' - const tokenData = window?.localStorage['supabase.auth.token'] - if (!tokenData) { + const { session } = await auth.getSession() + if (!session) { // try to get from url fragment - const access_token = getParameterByName('access_token') - if (access_token) return access_token - else return undefined + const accessToken = getParameterByName('access_token') + if (accessToken) { + return accessToken + } else { + return undefined + } } - const tokenObj = tryParseJson(tokenData) - if (tokenObj === false) { - return '' - } - return tokenObj.currentSession.access_token + + return session.access_token } // get param from URL fragment @@ -116,7 +117,7 @@ export function getParameterByName(name: string, url?: string) { return decodeURIComponent(results[2].replace(/\+/g, ' ')) } -export function constructHeaders(requestId: string, optionHeaders?: { [prop: string]: any }) { +export async function constructHeaders(requestId: string, optionHeaders?: { [prop: string]: any }) { let headers: { [prop: string]: any } = { 'Content-Type': 'application/json', Accept: 'application/json', @@ -126,7 +127,7 @@ export function constructHeaders(requestId: string, optionHeaders?: { [prop: str const hasAuthHeader = !isUndefined(optionHeaders) && 'Authorization' in optionHeaders if (!hasAuthHeader) { - const accessToken = getAccessToken() + const accessToken = await getAccessToken() if (accessToken) headers.Authorization = `Bearer ${accessToken}` } diff --git a/studio/lib/common/fetch/delete.ts b/studio/lib/common/fetch/delete.ts index d9b27abc1a4..ff5a2f3f4eb 100644 --- a/studio/lib/common/fetch/delete.ts +++ b/studio/lib/common/fetch/delete.ts @@ -10,7 +10,7 @@ export async function delete_( const requestId = uuidv4() try { const { headers: optionHeaders, ...otherOptions } = options ?? {} - const headers = constructHeaders(requestId, optionHeaders) + const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'DELETE', body: JSON.stringify(data), diff --git a/studio/lib/common/fetch/get.ts b/studio/lib/common/fetch/get.ts index 60b0777d129..dc4f56a0816 100644 --- a/studio/lib/common/fetch/get.ts +++ b/studio/lib/common/fetch/get.ts @@ -9,7 +9,7 @@ export async function get( const requestId = uuidv4() try { const { headers: optionHeaders, ...otherOptions } = options ?? {} - const headers = constructHeaders(requestId, optionHeaders) + const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'GET', credentials: 'include', @@ -34,7 +34,7 @@ export async function getWithTimeout( const controller = new AbortController() const id = setTimeout(() => controller.abort(), timeout) const { headers: optionHeaders, ...otherOptions } = options ?? {} - const headers = constructHeaders(requestId, optionHeaders) + const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'GET', credentials: 'include', diff --git a/studio/lib/common/fetch/head.ts b/studio/lib/common/fetch/head.ts index 9d8f62fefdc..6d7e7f42d6e 100644 --- a/studio/lib/common/fetch/head.ts +++ b/studio/lib/common/fetch/head.ts @@ -10,7 +10,7 @@ export async function head( const requestId = uuidv4() try { const { headers: optionHeaders, ...otherOptions } = options ?? {} - const headers = constructHeaders(requestId, optionHeaders) + const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'HEAD', credentials: 'include', @@ -36,7 +36,7 @@ export async function headWithTimeout( const controller = new AbortController() const id = setTimeout(() => controller.abort(), timeout) const { headers: optionHeaders, ...otherOptions } = options ?? {} - const headers = constructHeaders(requestId, optionHeaders) + const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'HEAD', credentials: 'include', diff --git a/studio/lib/common/fetch/patch.ts b/studio/lib/common/fetch/patch.ts index 6a6be18d24a..9ca15e0c989 100644 --- a/studio/lib/common/fetch/patch.ts +++ b/studio/lib/common/fetch/patch.ts @@ -10,7 +10,7 @@ export async function patch( const requestId = uuidv4() try { const { headers: optionHeaders, ...otherOptions } = options ?? {} - const headers = constructHeaders(requestId, optionHeaders) + const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'PATCH', body: JSON.stringify(data), diff --git a/studio/lib/common/fetch/post.ts b/studio/lib/common/fetch/post.ts index 69c12dd0204..e4757dc34a6 100644 --- a/studio/lib/common/fetch/post.ts +++ b/studio/lib/common/fetch/post.ts @@ -10,7 +10,7 @@ export async function post( const requestId = uuidv4() try { const { headers: optionHeaders, ...otherOptions } = options ?? {} - const headers = constructHeaders(requestId, optionHeaders) + const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'POST', body: JSON.stringify(data), diff --git a/studio/lib/gotrue.ts b/studio/lib/gotrue.ts index de73677ac57..358b7cb326e 100644 --- a/studio/lib/gotrue.ts +++ b/studio/lib/gotrue.ts @@ -10,7 +10,7 @@ export const auth = new GoTrueClient({ export const getAuthUser = async (token: String): Promise => { try { - const { data: user, error } = await auth.api.getUser(token.replace('Bearer ', '')) + const { user, error } = await auth.api.getUser(token.replace('Bearer ', '')) if (error) throw error return { user, error: null } diff --git a/studio/localStores/storageExplorer/StorageExplorerStore.js b/studio/localStores/storageExplorer/StorageExplorerStore.js index d16faf7fd30..bee36bb82ae 100644 --- a/studio/localStores/storageExplorer/StorageExplorerStore.js +++ b/studio/localStores/storageExplorer/StorageExplorerStore.js @@ -95,8 +95,12 @@ class StorageExplorerStore { constructor(projectRef) { makeAutoObservable(this, { supabaseClient: false }) this.projectRef = projectRef - this.abortController = new AbortController() this.ui = useStore().ui + + // ignore when in a non-browser environment + if (typeof window !== 'undefined') { + this.abortController = new AbortController() + } } initStore(projectRef, url, serviceKey) { diff --git a/studio/package-lock.json b/studio/package-lock.json index 109aea1e995..7f35ab1cee8 100644 --- a/studio/package-lock.json +++ b/studio/package-lock.json @@ -21,7 +21,7 @@ "@supabase/postgres-meta": "^0.26.1", "@supabase/react-data-grid": "^7.1.0-beta.7", "@supabase/shared-types": "^0.1.24", - "@supabase/supabase-js": "^1.35.3", + "@supabase/supabase-js": "^1.36.0-next.9", "@supabase/ui": "0.37.0-alpha.71", "ajv": "^8.6.3", "analytics-node": "^3.5.0", @@ -8320,19 +8320,19 @@ "integrity": "sha512-cywIoKu3sJnBPQ1eKi3BzFHWslA2ePqHvQhcxp7iYYlo1tWcVgEKTSh7y7hb6GoR4TyT3DwlK4v1vOZpVg8u4Q==" }, "node_modules/@supabase/functions-js": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz", - "integrity": "sha512-35vO9niHRtzGe1QSvXKdOfvGPiX2KC44dGpWU6y0/gZCfTIgog/soU9HqABzQC/maVowO3hGLWfez5aN0MKfow==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.4.tgz", + "integrity": "sha512-yYVgkECjv7IZEBKBI3EB5Q7R1p0FJ10g8Q9N7SWKIHUU6i6DnbEGHIMFLyQRm1hmiNWD8fL7bRVEYacmTRJhHw==", "dependencies": { "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/gotrue-js": { - "version": "1.22.15", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.15.tgz", - "integrity": "sha512-7/mwnd1hR/bpkCmbDvjnwPfWyRcE2B1ZnfxthqgVaZ5oJHS/CQibyuLBL8DA75fxmgY9nIfednDZSydSm6zK0w==", + "version": "1.23.0-next.4", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.23.0-next.4.tgz", + "integrity": "sha512-w+qCUc7BA0t1Tx08ylObz/XzmnVlTwkdDPrYlE/gC9Qr6n1MmuNNbmz6Odz66CtFQiDjS+62uTJbxJSnn/Mygg==", "dependencies": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/postgres-meta": { @@ -8350,11 +8350,11 @@ } }, "node_modules/@supabase/postgrest-js": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.2.tgz", - "integrity": "sha512-3Dgx5k3RvtKqc8DvR2BEyh2fVyjZe5P4e0zD1r8dyuVmpaYDaASZ2YeNVgyWXMCWH7xzrj4vepTYlKwfj78QLg==", + "version": "1.0.0-next.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.0.0-next.2.tgz", + "integrity": "sha512-8hg7xbT6w07BjrQRqQT4DOVg9NkcBFdvlpdArriKwOtqKiztVy32HXT/CjPTgMxl9Z4Bc7n2krhS5EKOB7Wmvw==", "dependencies": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/react-data-grid": { @@ -8370,9 +8370,9 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.2.tgz", - "integrity": "sha512-DMUaFIKj7KszGtWTTQbhMmUzZf7UnwYqySsmY+G8HgYxvY3ZaVa+DZD0I6ofgr4OLNr0po/ODM2a4lf5m5GNBg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.3.tgz", + "integrity": "sha512-iNUWhVeYRi5+XUlW2zJ7ccGfhI6caLxcn2t6VuQK3OTJNzXdVXeKb25nffLx1g4F7Ty6VM8Xiue7i0z0cWG3pQ==", "dependencies": { "@types/phoenix": "^1.5.4", "websocket": "^1.0.34" @@ -8384,23 +8384,24 @@ "integrity": "sha512-zvGWWLLh5ZAeiSR4VfJGBY9KbRb1Jh8//gJVmZwiaRv0i6F4i9qTsFOmWjpFgLY5+grAqwSfyIfB+jG8rxcidQ==" }, "node_modules/@supabase/storage-js": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.0.tgz", - "integrity": "sha512-f5EBw0wM96hKmnrXhgiqq2Reh9O0NgjKE+jkaKY4jQmfutefqaCAWn+cBzlmHs9h135H2ldaGmhWRFHUSkLt2g==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.2.tgz", + "integrity": "sha512-HX4HAfLUJznVgAwiKVgdTe5QD0bpUcqgc0hpk/s5Uy8qoe1tHZAc5qE9kI+tqk7rQKyymFpiA7+bAHlzyZXxxQ==", "dependencies": { "cross-fetch": "^3.1.0" } }, "node_modules/@supabase/supabase-js": { - "version": "1.35.3", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.3.tgz", - "integrity": "sha512-uwO8OVdMFsGZNZ1xQhFz22+PSW0EWYZ5xVq+jQeGz8nhabEu+Q9Uyep/bcNzOpyPJRzbGfxSPRzgAdAxfJgFhw==", + "version": "1.36.0-next.9", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.36.0-next.9.tgz", + "integrity": "sha512-GzaCNe7maoaao6MjX+MZoEd3EZnMCbathvXmh77vLOWD72HuQKVxUM77CR1cO/Ry8tZpbQlHkAiPKM122NSQkQ==", "dependencies": { "@supabase/functions-js": "^1.3.3", - "@supabase/gotrue-js": "^1.22.14", - "@supabase/postgrest-js": "^0.37.2", - "@supabase/realtime-js": "^1.7.2", - "@supabase/storage-js": "^1.7.0" + "@supabase/gotrue-js": "^1.23.0-next.3", + "@supabase/postgrest-js": "^1.0.0-next.2", + "@supabase/realtime-js": "^1.7.3", + "@supabase/storage-js": "^1.7.0", + "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/ui": { @@ -12531,17 +12532,17 @@ "node_modules/cross-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/cross-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/cross-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -13709,7 +13710,7 @@ "node_modules/es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "dependencies": { "d": "1", "es5-ext": "^0.10.35", @@ -30063,7 +30064,7 @@ "node_modules/yaeti": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", "engines": { "node": ">=0.10.32" } @@ -36227,19 +36228,19 @@ "integrity": "sha512-cywIoKu3sJnBPQ1eKi3BzFHWslA2ePqHvQhcxp7iYYlo1tWcVgEKTSh7y7hb6GoR4TyT3DwlK4v1vOZpVg8u4Q==" }, "@supabase/functions-js": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz", - "integrity": "sha512-35vO9niHRtzGe1QSvXKdOfvGPiX2KC44dGpWU6y0/gZCfTIgog/soU9HqABzQC/maVowO3hGLWfez5aN0MKfow==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.4.tgz", + "integrity": "sha512-yYVgkECjv7IZEBKBI3EB5Q7R1p0FJ10g8Q9N7SWKIHUU6i6DnbEGHIMFLyQRm1hmiNWD8fL7bRVEYacmTRJhHw==", "requires": { "cross-fetch": "^3.1.5" } }, "@supabase/gotrue-js": { - "version": "1.22.15", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.15.tgz", - "integrity": "sha512-7/mwnd1hR/bpkCmbDvjnwPfWyRcE2B1ZnfxthqgVaZ5oJHS/CQibyuLBL8DA75fxmgY9nIfednDZSydSm6zK0w==", + "version": "1.23.0-next.4", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.23.0-next.4.tgz", + "integrity": "sha512-w+qCUc7BA0t1Tx08ylObz/XzmnVlTwkdDPrYlE/gC9Qr6n1MmuNNbmz6Odz66CtFQiDjS+62uTJbxJSnn/Mygg==", "requires": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" } }, "@supabase/postgres-meta": { @@ -36253,11 +36254,11 @@ } }, "@supabase/postgrest-js": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.2.tgz", - "integrity": "sha512-3Dgx5k3RvtKqc8DvR2BEyh2fVyjZe5P4e0zD1r8dyuVmpaYDaASZ2YeNVgyWXMCWH7xzrj4vepTYlKwfj78QLg==", + "version": "1.0.0-next.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.0.0-next.2.tgz", + "integrity": "sha512-8hg7xbT6w07BjrQRqQT4DOVg9NkcBFdvlpdArriKwOtqKiztVy32HXT/CjPTgMxl9Z4Bc7n2krhS5EKOB7Wmvw==", "requires": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" } }, "@supabase/react-data-grid": { @@ -36269,9 +36270,9 @@ } }, "@supabase/realtime-js": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.2.tgz", - "integrity": "sha512-DMUaFIKj7KszGtWTTQbhMmUzZf7UnwYqySsmY+G8HgYxvY3ZaVa+DZD0I6ofgr4OLNr0po/ODM2a4lf5m5GNBg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.3.tgz", + "integrity": "sha512-iNUWhVeYRi5+XUlW2zJ7ccGfhI6caLxcn2t6VuQK3OTJNzXdVXeKb25nffLx1g4F7Ty6VM8Xiue7i0z0cWG3pQ==", "requires": { "@types/phoenix": "^1.5.4", "websocket": "^1.0.34" @@ -36283,23 +36284,24 @@ "integrity": "sha512-zvGWWLLh5ZAeiSR4VfJGBY9KbRb1Jh8//gJVmZwiaRv0i6F4i9qTsFOmWjpFgLY5+grAqwSfyIfB+jG8rxcidQ==" }, "@supabase/storage-js": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.0.tgz", - "integrity": "sha512-f5EBw0wM96hKmnrXhgiqq2Reh9O0NgjKE+jkaKY4jQmfutefqaCAWn+cBzlmHs9h135H2ldaGmhWRFHUSkLt2g==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.2.tgz", + "integrity": "sha512-HX4HAfLUJznVgAwiKVgdTe5QD0bpUcqgc0hpk/s5Uy8qoe1tHZAc5qE9kI+tqk7rQKyymFpiA7+bAHlzyZXxxQ==", "requires": { "cross-fetch": "^3.1.0" } }, "@supabase/supabase-js": { - "version": "1.35.3", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.3.tgz", - "integrity": "sha512-uwO8OVdMFsGZNZ1xQhFz22+PSW0EWYZ5xVq+jQeGz8nhabEu+Q9Uyep/bcNzOpyPJRzbGfxSPRzgAdAxfJgFhw==", + "version": "1.36.0-next.9", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.36.0-next.9.tgz", + "integrity": "sha512-GzaCNe7maoaao6MjX+MZoEd3EZnMCbathvXmh77vLOWD72HuQKVxUM77CR1cO/Ry8tZpbQlHkAiPKM122NSQkQ==", "requires": { "@supabase/functions-js": "^1.3.3", - "@supabase/gotrue-js": "^1.22.14", - "@supabase/postgrest-js": "^0.37.2", - "@supabase/realtime-js": "^1.7.2", - "@supabase/storage-js": "^1.7.0" + "@supabase/gotrue-js": "^1.23.0-next.3", + "@supabase/postgrest-js": "^1.0.0-next.2", + "@supabase/realtime-js": "^1.7.3", + "@supabase/storage-js": "^1.7.0", + "cross-fetch": "^3.1.5" } }, "@supabase/ui": { @@ -39700,17 +39702,17 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -40684,7 +40686,7 @@ "es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "requires": { "d": "1", "es5-ext": "^0.10.35", @@ -52987,7 +52989,7 @@ "yaeti": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==" }, "yallist": { "version": "4.0.0", diff --git a/studio/package.json b/studio/package.json index fae49c445ed..b66f0a8c9fe 100644 --- a/studio/package.json +++ b/studio/package.json @@ -27,7 +27,7 @@ "@supabase/postgres-meta": "^0.26.1", "@supabase/react-data-grid": "^7.1.0-beta.7", "@supabase/shared-types": "^0.1.24", - "@supabase/supabase-js": "^1.35.3", + "@supabase/supabase-js": "^1.36.0-next.9", "@supabase/ui": "0.37.0-alpha.71", "ajv": "^8.6.3", "analytics-node": "^3.5.0", diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 0d92bfdc20e..a9164e58828 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -26,12 +26,7 @@ import { StoreProvider } from 'hooks' import { getParameterByName } from 'lib/common/fetch' import { GOTRUE_ERRORS } from 'lib/constants' -import { - PortalToast, - GoTrueWrapper, - RouteValidationWrapper, - AppBannerWrapper, -} from 'components/interfaces/App' +import { PortalToast, RouteValidationWrapper, AppBannerWrapper } from 'components/interfaces/App' import PageTelemetry from 'components/ui/PageTelemetry' import FlagProvider from 'components/ui/Flag/FlagProvider' @@ -64,13 +59,11 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { - - - - {getLayout()} - - - + + + {getLayout()} + + diff --git a/studio/pages/index.tsx b/studio/pages/index.tsx index 542247844aa..c7660240b37 100644 --- a/studio/pages/index.tsx +++ b/studio/pages/index.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite' import { NextRouter, useRouter } from 'next/router' import { NextPageWithLayout } from 'types' -import { useStore, withAuth } from 'hooks' +import { useProfile, useStore, withAuth } from 'hooks' import { auth } from 'lib/gotrue' import { IS_PLATFORM } from 'lib/constants' @@ -72,10 +72,12 @@ const UnauthorizedLanding = () => { const IndexLayout = withAuth( observer(({ children }) => { - const { ui } = useStore() - const { profile } = ui - const router = useRouter() + const { profile, isLoading } = useProfile() + + if (isLoading) { + return + } if (!profile) { return @@ -98,6 +100,7 @@ const IndexLayout = withAuth( } else { router.push('/') } + return } } diff --git a/studio/pages/org/[slug]/settings.tsx b/studio/pages/org/[slug]/settings.tsx index dc8dff77e13..a4fda269a5b 100644 --- a/studio/pages/org/[slug]/settings.tsx +++ b/studio/pages/org/[slug]/settings.tsx @@ -1,55 +1,46 @@ +import Image from 'next/image' import { createContext, useEffect, useContext, useState } from 'react' import { useRouter } from 'next/router' import { observer, useLocalObservable } from 'mobx-react-lite' import { toJS } from 'mobx' -import { pluckJsonSchemaFields, pluckObjectFields, timeout } from 'lib/helpers' +import { pluckJsonSchemaFields, pluckObjectFields } from 'lib/helpers' import { AutoField } from 'uniforms-bootstrap4' import { organizations } from 'stores/jsonSchema' import { Loading, Button, Badge, - IconMoreHorizontal, Tabs, Typography, - IconTrash, Alert, Input, - Dropdown, - Modal, IconSearch, IconUser, - Form, } from '@supabase/ui' -import { API_URL } from 'lib/constants' import { useOrganizationDetail, useStore, withAuth } from 'hooks' -import { post, delete_, patch } from 'lib/common/fetch' +import { NextPageWithLayout, Project } from 'types' +import { API_URL } from 'lib/constants' +import { post, patch } from 'lib/common/fetch' + +import Panel from 'components/ui/Panel' import { AccountLayoutWithoutAuth } from 'components/layouts' -import { BillingSettings, InvoicesSettings } from 'components/interfaces/Organization' +import { + BillingSettings, + InvoicesSettings, + OwnerDropdown, + InviteMemberModal, + DeleteOrganizationButton, +} from 'components/interfaces/Organization' import Table from 'components/to-be-cleaned/Table' -import Panel from 'components/ui/Panel' -import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' -import InviteMemberModal from 'components/interfaces/Organization/InviteMemberModal' -import TextConfirmModal from 'components/ui/Modals/TextConfirmModal' import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel' -import { Member, NextPageWithLayout, Project } from 'types' -import Image from 'next/image' +import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' +import { isInviteExpired } from 'components/interfaces/Organization/Organization.utils' // [Joshen] Low prio refactor: Bring out general and team settings into their own components too -const PageContext = createContext(null) - -// Invite is expired if older than 24hrs -function inviteExpired(timestamp: Date) { - const inviteDate = new Date(timestamp) - const now = new Date() - var timeBetween = now.valueOf() - inviteDate.valueOf() - if (timeBetween / 1000 / 60 / 60 < 24) { - return true - } -} +export const PageContext = createContext(null) const OrgSettingsLayout = withAuth( observer(({ children }) => { @@ -179,22 +170,10 @@ const TabsView = observer(() => {
@@ -289,123 +268,13 @@ const OrgDeletePanel = observer(() => {

Make sure you have made a backup if you want to keep your data

- + ) }) -const OrgDeleteModal = observer(() => { - const PageState: any = useContext(PageContext) - const router = useRouter() - const { ui } = useStore() - - const { slug: orgSlug, name: orgName } = PageState.organization - - const [isOpen, setIsOpen] = useState(false) - const [value, setValue] = useState('') - - function toggle() { - setIsOpen(!isOpen) - } - - return ( - <> -
- -
- -
Delete organisation
- Are you sure? -
- } - size="small" - hideFooter - closable - > -
{ - setSubmitting(true) - const response = await delete_(`${API_URL}/organizations/${orgSlug}`) - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to delete organization: ${response.error.message}`, - }) - setSubmitting(false) - } else { - PageState.onOrgDeleted(PageState.organization) - setSubmitting(false) - router.push('/') - } - }} - validate={(values) => { - const errors: any = {} - if (!values.orgName) { - errors.orgName = 'Enter the name of the organization.' - } - if (values.orgName !== orgSlug) { - errors.orgName = 'Value entered does not match name of the organization.' - } - return errors - }} - > - {({ isSubmitting }: { isSubmitting: boolean }) => ( -
- -

- This action cannot be undone. This will - permanently delete the {orgName}{' '} - organization and remove all of its projects. -

-
- - - - Please type {orgSlug} to confirm - - } - onChange={(e) => setValue(e.target.value)} - value={value} - placeholder="Type in the orgnaization name" - className="w-full" - /> - - - - - -
- )} -
- - - ) -}) - const TeamSettings = observer(() => { const PageState: any = useContext(PageContext) const { ui } = useStore() @@ -532,8 +401,8 @@ const MembersView = observer(() => { {x.invited_id && ( - - {inviteExpired(x.invited_at) ? 'Invited' : 'Expired'} + + {isInviteExpired(x.invited_at) ? 'Invited' : 'Expired'} )} @@ -572,191 +441,3 @@ const MembersView = observer(() => {
) }) - -const OwnerDropdown = observer(({ members, member }: any) => { - const PageState: any = useContext(PageContext) - const { ui } = useStore() - const { mutateOrgMembers } = useOrganizationDetail(ui.selectedOrganization?.slug || '') - const [isOpen, setIsOpen] = useState(false) - const [loading, setLoading] = useState(false) - - // handle modal visibility - const [ownerTransferIsVisble, setOwnerTransferIsVisble] = useState(false) - - const { id: orgId, slug: orgSlug, stripe_customer_id, name: orgName } = PageState.organization - - async function handleMemberDelete() { - setIsOpen(false) - await timeout(200) - - confirmAlert({ - title: 'Confirm to remove', - message: `This is permanent! Are you sure you want to remove ${member.profile.primary_email}?`, - onAsyncConfirm: async () => { - setLoading(true) - const response = await delete_(`${API_URL}/organizations/${orgSlug}/members/remove`, { - member_id: member.id, - }) - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to delete user: ${response.error.message}`, - }) - setLoading(false) - } else { - const updatedMembers = members.filter((x: any) => x.id !== member.id) - mutateOrgMembers(updatedMembers) - ui.setNotification({ category: 'success', message: 'Successfully removed member' }) - } - }, - }) - } - - async function handleTransfer() { - setLoading(true) - - const response = await post(`${API_URL}/organizations/${orgSlug}/transfer`, { - org_id: orgId, - member_id: member.id, - }) - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to transfer ownership: ${response.error.message}`, - }) - setLoading(false) - } else { - const updatedMembers = [...members] - const oldOwner = updatedMembers.find((x) => x.is_owner == true) - if (oldOwner) oldOwner.is_owner = false - const newOwner = updatedMembers.find((x) => x.id == member.id) - if (newOwner) newOwner.is_owner = true - mutateOrgMembers(updatedMembers) - setOwnerTransferIsVisble(false) - ui.setNotification({ category: 'success', message: 'Successfully transfered organization' }) - } - } - - async function handleResendInvite(member: Member) { - setLoading(true) - - const response = await post(`${API_URL}/organizations/${orgSlug}/members/invite`, { - invited_email: member.profile.primary_email, - owner_id: member.invited_id, - }) - - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to resend invitation: ${response.error.message}`, - }) - setLoading(false) - } else { - const updatedMembers = [...members] - mutateOrgMembers(updatedMembers) - ui.setNotification({ category: 'success', message: 'Resent the invitation.' }) - setLoading(false) - } - } - - async function handleRevokeInvitation(id: number) { - setLoading(true) - - const response = await delete_( - `${API_URL}/organizations/${orgSlug}/members/invite?invited_id=${id}`, - {} - ) - - if (response.error) { - ui.setNotification({ - category: 'error', - message: `Failed to revoke invitation: ${response.error.message}`, - }) - setLoading(false) - } else { - const updatedMembers = [...members] - mutateOrgMembers(updatedMembers) - ui.setNotification({ category: 'success', message: 'Successfully revoked the invitation.' }) - } - } - - return ( -
- - {!member.invited_at && ( - setOwnerTransferIsVisble(!ownerTransferIsVisble)}> -
-

Make owner

-

Transfer ownership of "{orgName}"

-
-
- )} - - {member.invited_at && ( - <> - handleRevokeInvitation(member.invited_id)}> -
-

Cancel invitation

-

Revoke this invitation.

-
-
- - {!inviteExpired(member.invited_at) && ( - <> - - handleResendInvite(member)}> -
-

Resend invitation

-

Invites expire after 24hrs.

-
-
- - )} - - )} - - {!member.invited_at && ( - <> - - } onClick={handleMemberDelete}> - Remove member - - - )} - - } - > - -
- - setOwnerTransferIsVisble(!ownerTransferIsVisble)} - onConfirm={handleTransfer} - alert="Payment methods such as credit cards will also be transferred. You may want to delete credit card information first before transferring." - text={ - - By transferring this organization, it will be solely owned by{' '} - {member.profile?.username}, they - will also be able to remove you from the organization as a member - - } - /> -
- ) -}) diff --git a/studio/pages/project/[ref]/editor/[id].tsx b/studio/pages/project/[ref]/editor/[id].tsx index b0aadab9925..77d1b2591bb 100644 --- a/studio/pages/project/[ref]/editor/[id].tsx +++ b/studio/pages/project/[ref]/editor/[id].tsx @@ -36,7 +36,9 @@ const TableEditorPage: NextPage = () => { const selectedTable = !isNaN(Number(id)) ? // @ts-ignore tables.find((table) => table.id === Number(id)) - : tryParseJson(Base64.decode(id)) + : id !== undefined + ? tryParseJson(Base64.decode(id)) + : undefined useEffect(() => { if (selectedTable && 'schema' in selectedTable) { diff --git a/studio/pages/project/[ref]/settings/api.tsx b/studio/pages/project/[ref]/settings/api.tsx index 1a04c006b55..02039f27d6e 100644 --- a/studio/pages/project/[ref]/settings/api.tsx +++ b/studio/pages/project/[ref]/settings/api.tsx @@ -545,14 +545,10 @@ const PostgrestConfig = observer(({ config, projectRef }: any) => { } emptyMessage={ <> - - - - No schema available to choose -
- - New schema you create will appear here - + +
+

No schema available to choose

+

New schemas you create will appear here

} diff --git a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx index 28f801d5e56..0dd9964fcd4 100644 --- a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx +++ b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx @@ -86,6 +86,7 @@ const PROVIDER_PHONE = { SMS_TWILIO_AUTH_TOKEN: { type: 'string', title: 'Twilio Auth Token', + isSecret: true, show: { key: 'SMS_PROVIDER', matches: 'twilio', @@ -311,6 +312,7 @@ Client identifier when authenticating or validating users. The secret key is a JWT token that must be generated. [Learn more](https://supabase.com/docs/guides/auth/auth-apple#generate-a-client_secret)`, type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -356,6 +358,7 @@ const EXTERNAL_PROVIDER_AZURE = { // [TODO] Change docs title: 'Secret ID', type: 'string', + isSecret: true, }, EXTERNAL_AZURE_URL: { // [TODO] Change docs @@ -400,6 +403,7 @@ const EXTERNAL_PROVIDER_BITBUCKET = { EXTERNAL_BITBUCKET_SECRET: { title: 'Secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -437,6 +441,7 @@ const EXTERNAL_PROVIDER_DISCORD = { EXTERNAL_DISCORD_SECRET: { title: 'Client Secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -474,6 +479,7 @@ const EXTERNAL_PROVIDER_FACEBOOK = { EXTERNAL_FACEBOOK_SECRET: { title: 'Facebook secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -511,6 +517,7 @@ const EXTERNAL_PROVIDER_GITHUB = { EXTERNAL_GITHUB_SECRET: { title: 'Client Secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -550,6 +557,7 @@ const EXTERNAL_PROVIDER_GITLAB = { EXTERNAL_GITLAB_SECRET: { title: 'Secret', type: 'string', + isSecret: true, }, EXTERNAL_GITLAB_URL: { title: 'Self Hosted GitLab URL', @@ -595,6 +603,7 @@ const EXTERNAL_PROVIDER_GOOGLE = { EXTERNAL_GOOGLE_SECRET: { title: 'Client Secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -633,6 +642,7 @@ const EXTERNAL_PROVIDER_KEYCLOAK = { EXTERNAL_KEYCLOAK_SECRET: { title: 'Secret', type: 'string', + isSecret: true, }, EXTERNAL_KEYCLOAK_URL: { title: 'Realm URL', @@ -683,6 +693,7 @@ const EXTERNAL_PROVIDER_LINKEDIN = { EXTERNAL_LINKEDIN_SECRET: { title: 'API Secret Key', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -720,6 +731,7 @@ const EXTERNAL_PROVIDER_NOTION = { EXTERNAL_NOTION_SECRET: { title: 'OAuth client secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -757,6 +769,7 @@ const EXTERNAL_PROVIDER_TWITCH = { EXTERNAL_TWITCH_SECRET: { title: 'Client secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -794,6 +807,7 @@ const EXTERNAL_PROVIDER_TWITTER = { EXTERNAL_TWITTER_SECRET: { title: 'API Secret Key', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -831,6 +845,7 @@ const EXTERNAL_PROVIDER_SLACK = { EXTERNAL_SLACK_SECRET: { title: 'Client Secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -868,6 +883,7 @@ const EXTERNAL_PROVIDER_SPOTIFY = { EXTERNAL_SPOTIFY_SECRET: { title: 'Client Secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -909,6 +925,7 @@ const EXTERNAL_PROVIDER_WORKOS = { EXTERNAL_WORKOS_SECRET: { title: 'Secret Key', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ @@ -953,6 +970,7 @@ const EXTERNAL_PROVIDER_ZOOM = { EXTERNAL_ZOOM_SECRET: { title: 'Client secret', type: 'string', + isSecret: true, }, }, validationSchema: object().shape({ diff --git a/studio/tests/components/Auth/Auth.constants.test.js b/studio/tests/components/Auth/Auth.constants.test.js index e10824d0d1e..842200dd2ef 100644 --- a/studio/tests/components/Auth/Auth.constants.test.js +++ b/studio/tests/components/Auth/Auth.constants.test.js @@ -45,6 +45,18 @@ describe('Auth.constants: domainRegex', () => { const mockInput2 = 'https://supabase.com/*' const output2 = domainRegex.test(mockInput2) expect(output2).toBe(true) + + const mockInput3 = 'https://new-*-domain.com/*' + const output3 = domainRegex.test(mockInput3) + expect(output3).toBe(true) + + const mockInput4 = 'https://new-*-domain.com/*/*/*' + const output4 = domainRegex.test(mockInput4) + expect(output4).toBe(true) + + const mockInput5 = 'https://sub-*-domain.new-*-domain.com/*/*/*' + const output5 = domainRegex.test(mockInput5) + expect(output5).toBe(true) }) test('should invalidate invalid URLs', () => { const mockInput1 = 'supabase'