Revert "Revert "chore: remove global loading spinner""

This reverts commit c5be38e52a.
This commit is contained in:
Joshen Lim authored and GitHub committed 2022-07-18 20:34:37 +08:00
1 parent c5be38e52a
commit 2f15159fca
37 files changed
+876 -835

No files matched your search

@@ -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 ? <Connecting /> : children}</>
}
export default GoTrueWrapper
@@ -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'
}
+1 -2
View File
@@ -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 }
@@ -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
@@ -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<Props> = ({ 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 (
<Input
size="small"
layout="vertical"
id={name}
name={name}
type={hidden ? 'password' : 'text'}
label={properties.title}
labelOptional={
properties.descriptionOptional ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.descriptionOptional}
</ReactMarkdown>
) : null
}
descriptionText={
properties.description ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.description}
</ReactMarkdown>
) : null
}
actions={
!!properties.isSecret ? (
<Button
icon={hidden ? <IconEye /> : <IconEyeOff />}
type="default"
onClick={() => setHidden(!hidden)}
/>
) : (
<span className="text-scale-900 mr-3">
{properties.units ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.units}
</ReactMarkdown>
) : null}
</span>
)
}
/>
)
case 'number':
return (
<InputNumber
size="small"
layout="vertical"
id={name}
name={name}
label={properties.title}
labelOptional={
properties.descriptionOptional ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.descriptionOptional}
</ReactMarkdown>
) : null
}
descriptionText={
properties.description ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.description}
</ReactMarkdown>
) : null
}
actions={
<span className="text-scale-900 mr-3">
{properties.units ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.units}
</ReactMarkdown>
) : null}
</span>
}
/>
)
case 'boolean':
return (
<Toggle
size="small"
id={name}
name={name}
label={properties.title}
descriptionText={properties.description}
/>
)
case 'select':
return (
<Listbox
size="small"
name={name}
label={properties.title}
descriptionText={properties.description}
defaultValue={properties.enum[0]}
>
{properties.enum.map((option: Enum) => {
return (
<Listbox.Option
id={option.value}
key={option.value}
label={option.label}
value={option.value}
addOnBefore={() => <img className="h-6 w-6" src={`/img/icons/${option.icon}`} />}
>
{option.label}
</Listbox.Option>
)
})}
</Listbox>
)
default:
break
}
return <></>
}
export default FormField
@@ -1,22 +1,11 @@
import { FC, useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import {
Alert,
Button,
Collapsible,
Form,
IconCheck,
IconChevronUp,
Input,
InputNumber,
Listbox,
Toggle,
} from '@supabase/ui'
import { Alert, Button, Collapsible, Form, IconCheck, IconChevronUp, Input } from '@supabase/ui'
import { useStore } from 'hooks'
import { Enum, Provider } from './AuthProvidersForm.types'
import { Provider } from './AuthProvidersForm.types'
import { ProviderCollapsibleClasses } from './AuthProvidersForm.constants'
import { createConsoleLogger } from 'configcat-js'
import FormField from './FormField'
interface Props {
provider: Provider
@@ -129,128 +118,14 @@ const ProviderForm: FC<Props> = ({ provider }) => {
"
>
<div className="mx-auto my-6 max-w-md space-y-6">
{Object.keys(provider.properties).map((x: string) => {
const properties = provider.properties[x]
// Conditionally hide properties based on value of key
if (
properties.show &&
values[properties.show.key] !== properties.show.matches
) {
return null
}
switch (properties.type) {
case 'string':
return (
<Input
size="small"
layout="vertical"
id={x}
key={x}
name={x}
label={properties.title}
labelOptional={
properties.descriptionOptional ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.descriptionOptional}
</ReactMarkdown>
) : null
}
descriptionText={
properties.description ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.description}
</ReactMarkdown>
) : null
}
actions={
<span className="text-scale-900 mr-3">
{properties.units ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.units}
</ReactMarkdown>
) : null}
</span>
}
/>
)
case 'number':
return (
<InputNumber
size="small"
layout="vertical"
id={x}
key={x}
name={x}
label={properties.title}
labelOptional={
properties.descriptionOptional ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.descriptionOptional}
</ReactMarkdown>
) : null
}
descriptionText={
properties.description ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.description}
</ReactMarkdown>
) : null
}
actions={
<span className="text-scale-900 mr-3">
{properties.units ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{properties.units}
</ReactMarkdown>
) : null}
</span>
}
/>
)
case 'boolean':
return (
<Toggle
size="small"
key={x}
name={x}
label={properties.title}
descriptionText={properties.description}
/>
)
case 'select':
return (
<Listbox
size="small"
key={x}
name={x}
label={properties.title}
descriptionText={properties.description}
defaultValue={properties.enum[0]}
>
{properties.enum.map((option: Enum) => {
return (
<Listbox.Option
id={option.value}
label={option.label}
value={option.value}
addOnBefore={() => (
<img className="h-6 w-6" src={`/img/icons/${option.icon}`} />
)}
>
{option.label}
</Listbox.Option>
)
})}
</Listbox>
)
default:
break
}
})}
{Object.keys(provider.properties).map((x: string) => (
<FormField
key={x}
name={x}
properties={provider.properties[x]}
formValues={values}
/>
))}
{provider?.misc?.alert && (
<Alert title={provider.misc.alert.title} variant="warning" withIcon>
<ReactMarkdown>{provider.misc.alert.description}</ReactMarkdown>
@@ -76,66 +76,62 @@ const TemplateEditor: FC<Props> = ({ template }) => {
return (
<>
<FormSection
className="!border-t-0"
header={
<FormSectionLabel>
<span>SMTP Provider Settings</span>
<p className="text-scale-900 my-4">
Your SMTP Credentials will always be encrypted in our database.
</p>
</FormSectionLabel>
}
>
<FormSection className="!border-t-0 pb-8 pt-4">
<FormSectionContent loading={!isLoaded}>
{Object.keys(properties).map((x: string) => {
const property = properties[x]
if (property.type === 'string') {
return (
<Input
size="small"
layout="vertical"
id={x}
key={x}
name={x}
label={property.title}
descriptionText={
property.description ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{property.description}
</ReactMarkdown>
) : null
}
labelOptional={
property.descriptionOptional ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{property.descriptionOptional}
</ReactMarkdown>
) : null
}
/>
<div className="space-y-3">
<label className="text-scale-1200 col-span-12 text-sm lg:col-span-5">
{property.title}
</label>
<Input
size="small"
layout="vertical"
id={x}
key={x}
name={x}
descriptionText={
property.description ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{property.description}
</ReactMarkdown>
) : null
}
labelOptional={
property.descriptionOptional ? (
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
{property.descriptionOptional}
</ReactMarkdown>
) : null
}
/>
</div>
)
}
})}
</FormSectionContent>
</FormSection>
<FormSection className="!mt-0 grid-cols-12 !border-t-0 !pt-0">
<FormSectionContent loading={!isLoaded} fullWidth>
<FormSectionContent fullWidth loading={!isLoaded}>
{messageProperty && (
<>
<FormSectionLabel>
<span>{messageProperty.title}</span>
</FormSectionLabel>
<InformationBox
title={'Message variables'}
hideCollapse={false}
defaultVisibility={true}
description={
messageProperty.description && (
<ReactMarkdown>{messageProperty.description}</ReactMarkdown>
)
}
/>
<div className="space-y-3">
<FormSectionLabel>
<span>{messageProperty.title}</span>
</FormSectionLabel>
<InformationBox
defaultVisibility
title="Message variables"
hideCollapse={false}
description={
messageProperty.description && (
<ReactMarkdown>{messageProperty.description}</ReactMarkdown>
)
}
/>
</div>
<div className="relative h-96">
<CodeEditor
id="code-id"
@@ -1,7 +1,7 @@
import { Alert, Form, Input, InputNumber, Toggle } from '@supabase/ui'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import { number, object, string } from 'yup'
import { Alert, Button, Form, Input, InputNumber, Toggle, IconEye, IconEyeOff } from '@supabase/ui'
import {
FormActions,
@@ -18,9 +18,11 @@ import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils'
const SmtpForm = () => {
const { authConfig, ui } = useStore()
const [enableSmtp, setEnableSmtp] = useState(false)
const { config, isLoaded } = authConfig
const [enableSmtp, setEnableSmtp] = useState(false)
const [hidden, setHidden] = useState(true)
const formId = 'auth-config-smtp-form'
const initialValues = generateFormValues(authConfig.config)
@@ -105,6 +107,7 @@ const SmtpForm = () => {
await authConfig.update(payload)
ui.setNotification({ category: 'success', message: 'Successfully updated settings' })
setHidden(true)
const updatedFormValues = generateFormValues(payload)
resetForm({ values: updatedFormValues, initialValues: updatedFormValues })
} catch (error) {
@@ -265,8 +268,16 @@ const SmtpForm = () => {
<Input
name="SMTP_PASS"
id="SMTP_PASS"
type={hidden ? 'password' : 'text'}
label="Password"
placeholder="SMTP Password"
actions={
<Button
icon={hidden ? <IconEye /> : <IconEyeOff />}
type="default"
onClick={() => setHidden(!hidden)}
/>
}
/>
</FormSectionContent>
</FormSection>
@@ -4,10 +4,10 @@ import { useState } from 'react'
const CommandRender = ({ commands }: any) => {
return (
<div className="space-y-4">
{commands.map((item: any) => {
{commands.map((item: any, idx: number) => {
const [isCopied, setIsCopied] = useState(false)
return (
<div className="space-y-1">
<div key={`command-${idx}`} className="space-y-1">
<span className="font-mono text-sm text-scale-900">{`> ${item.comment}`}</span>
<div className="flex items-center gap-2">
<div className="flex gap-2 text-scale-1200 font-mono text-sm font-normal">
@@ -0,0 +1,123 @@
import { useState, useContext } from 'react'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Button, Form, Modal, Input } from '@supabase/ui'
import { useStore } from 'hooks'
import { delete_ } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { PageContext } from 'pages/org/[slug]/settings'
const DeleteOrganizationButton = 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('')
const onValidate = (values: any) => {
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
}
const onConfirmDelete = async (values: any, { setSubmitting }: any) => {
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('/')
ui.setNotification({
category: 'success',
message: `Successfully deleted ${orgName}`,
})
}
}
return (
<>
<div className="mt-2">
<Button onClick={() => setIsOpen(true)} type="danger">
Delete organization
</Button>
</div>
<Modal
closable
hideFooter
size="small"
visible={isOpen}
onCancel={() => setIsOpen(false)}
header={
<div className="flex items-baseline gap-2">
<h5 className="text-scale-1200 text-sm">Delete organisation</h5>
<span className="text-scale-900 text-xs">Are you sure?</span>
</div>
}
>
<Form
validateOnBlur
initialValues={{ orgName: '' }}
onSubmit={onConfirmDelete}
validate={onValidate}
>
{({ isSubmitting }: { isSubmitting: boolean }) => (
<div className="space-y-4 py-3">
<Modal.Content>
<p className="text-scale-900 text-sm">
This action <span className="text-scale-1200">cannot</span> be undone. This will
permanently delete the <span className="text-scale-1200">{orgName}</span>{' '}
organization and remove all of its projects.
</p>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
<Input
id="orgName"
label={
<span>
Please type <span className="font-bold">{orgSlug}</span> to confirm
</span>
}
onChange={(e) => setValue(e.target.value)}
value={value}
placeholder="Type in the orgnaization name"
className="w-full"
/>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
<Button
block
danger
size="small"
type="danger"
htmlType="submit"
loading={isSubmitting}
>
I understand, delete this organization
</Button>
</Modal.Content>
</div>
)}
</Form>
</Modal>
</>
)
})
export default DeleteOrganizationButton
@@ -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
}
}
@@ -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 (
<div className="flex items-center justify-end">
<Dropdown
side="bottom"
align="end"
overlay={
<>
{!member.invited_at && (
<Dropdown.Item onClick={() => setOwnerTransferIsVisble(!ownerTransferIsVisble)}>
<div className="flex flex-col">
<p>Make owner</p>
<p className="block opacity-50">Transfer ownership of "{orgName}"</p>
</div>
</Dropdown.Item>
)}
{member.invited_at && (
<>
<Dropdown.Item onClick={() => handleRevokeInvitation(member.invited_id)}>
<div className="flex flex-col">
<p>Cancel invitation</p>
<p className="block opacity-50">Revoke this invitation.</p>
</div>
</Dropdown.Item>
{!isInviteExpired(member.invited_at) && (
<>
<Dropdown.Seperator />
<Dropdown.Item onClick={() => handleResendInvite(member)}>
<div className="flex flex-col">
<p>Resend invitation</p>
<p className="block opacity-50">Invites expire after 24hrs.</p>
</div>
</Dropdown.Item>
</>
)}
</>
)}
{!member.invited_at && (
<>
<Dropdown.Seperator />
<Dropdown.Item icon={<IconTrash size="tiny" />} onClick={handleMemberDelete}>
Remove member
</Dropdown.Item>
</>
)}
</>
}
>
<Button
as="span"
disabled={loading}
loading={loading}
type="text"
icon={<IconMoreHorizontal />}
></Button>
</Dropdown>
<TextConfirmModal
title="Transfer organization"
visible={ownerTransferIsVisble}
confirmString={orgSlug}
loading={loading}
confirmLabel="I understand, transfer ownership"
confirmPlaceholder="Type in name of orgnization"
onCancel={() => 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={
<span>
By transferring this organization, it will be solely owned by{' '}
<span className="font-medium dark:text-white">{member.profile?.username}</span>, they
will also be able to remove you from the organization as a member
</span>
}
/>
</div>
)
})
export default OwnerDropdown
@@ -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,
}
@@ -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<Object>} 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<Props> = ({ 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 (
<>
<Head>
@@ -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"
>
<WithSidebar title={title} breadcrumbs={breadcrumbs} links={linksWithHeaders}>
<WithSidebar title={title} breadcrumbs={breadcrumbs} links={sidebarLinks}>
{children}
</WithSidebar>
</main>
@@ -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
}
@@ -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<Props> = ({
id,
label,
href,
isActive = false,
isSubitem = false,
isExternal = false,
onClick = () => {},
}) => {
if (href === undefined) {
const icon = isExternal ? (
<IconArrowUpRight size="tiny" />
) : label === 'Logout' ? (
<IconLogOut size="tiny" />
) : undefined
return (
<Menu.Item
rounded
key={id}
style={{ marginLeft: isSubitem ? '.5rem' : '0' }}
active={isActive}
onClick={onClick}
icon={icon}
>
{isSubitem ? <p className="text-sm">{label}</p> : label}
</Menu.Item>
)
}
return (
<Link href={href || ''}>
<a className="block" target={isExternal ? '_blank' : '_self'}>
<button
className={[
'ring-scale-1200 border-scale-500 group-hover:border-scale-900 group',
'flex max-w-full cursor-pointer items-center space-x-2 py-1 font-normal',
'outline-none focus-visible:z-10 focus-visible:ring-1',
].join(' ')}
onClick={onClick}
>
{isExternal && (
<span className="text-scale-900 group-hover:text-scale-1100 truncate text-sm transition">
<IconArrowUpRight size="tiny" />
</span>
)}
<span
title={label}
className="text-scale-1100 group-hover:text-scale-1200 w-full truncate text-sm transition"
>
{isSubitem ? <p>{label}</p> : label}
</span>
</button>
</a>
</Link>
)
}
export default SidebarItem
@@ -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<Props> = ({
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<Props> = ({
].join(' ')}
>
{title && (
<div className="mb-2">
<div className="dark:border-dark flex h-12 max-h-12 items-center border-b px-6">
<Typography.Title level={4} className="mb-0">
{title}
</Typography.Title>
</div>
<div className="dark:border-dark flex h-12 max-h-12 items-center border-b px-6 mb-2">
<h4>{title}</h4>
</div>
)}
{header && header}
{header !== undefined && header}
<div className="-mt-1">
<Menu>
{customSidebarContent}
{links && linksHaveHeaders ? (
<LinksWithHeaders
links={links}
links={links as SidebarLinkGroup[]}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
/>
) : null}
{!linksHaveHeaders && links ? (
<LinksWithoutHeaders
links={links}
links={links as SidebarLink[]}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
/>
@@ -86,8 +83,8 @@ const WithSidebar: FC<Props> = ({
export default WithSidebar
const LinksWithHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) => {
return links.map((x: any) => (
<div key={x.heading} className="dark:border-dark border-b py-5 px-6">
return links.map((x: any, i: number) => (
<div key={x.heading || `heading_${i}`} className="dark:border-dark border-b py-5 px-6">
{x.heading && <Menu.Group title={x.heading} />}
{x.versionLabel && (
<div className="mb-1 px-3">
@@ -104,23 +101,26 @@ const LinksWithHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) => {
</div>
))
}
const LinksWithoutHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) => {
const LinksWithoutHeaders: FC<{
links: SidebarLink[]
subitems?: any[]
subitemsParentKey?: number
}> = ({ links, subitems, subitemsParentKey }) => {
return (
<ul className="space-y-1">
{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 = (
<SidebarItem
key={`${x.key || x.as}-${i}-sidebarItem`}
id={`${x.key || x.as}-${i}`}
slug={x.key}
key={`${x.key}-${i}-sidebarItem`}
id={`${x.key}-${i}`}
isActive={isActive}
label={x.label}
href={x.href}
onClick={x.onClick}
external={x.external || false}
isExternal={x.isExternal || false}
/>
)
@@ -129,11 +129,10 @@ const LinksWithoutHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) =>
<SidebarItem
key={`${y.key || y.as}-${i}-sidebarItem`}
id={`${y.key || y.as}-${i}`}
slug={y.key}
isSubitem={true}
label={y.label}
onClick={y.onClick}
external={x.external || false}
isSubitem={true}
isExternal={x.isExternal || false}
/>
))
render = [render, ...subItemsRender]
@@ -144,54 +143,3 @@ const LinksWithoutHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) =>
</ul>
)
}
const SidebarItem: FC<any> = ({ id, label, href, isActive, isSubitem, onClick, external }) => {
if (isUndefined(href)) {
let icon
if (external) {
icon = <IconArrowUpRight size={'tiny'} />
}
if (label === 'Logout') {
icon = <IconLogOut size={'tiny'} />
}
return (
<Menu.Item
rounded
key={id}
style={{
marginLeft: isSubitem && '.5rem',
}}
active={isActive ? true : false}
onClick={onClick || (() => {})}
icon={icon}
>
{isSubitem ? <p className="text-sm">{label}</p> : label}
</Menu.Item>
)
}
return (
<Link href={href || ''}>
<a className="block" target={external ? '_blank' : '_self'}>
<button
className="ring-scale-1200 border-scale-500 group-hover:border-scale-900 group flex max-w-full cursor-pointer items-center space-x-2 py-1 font-normal outline-none focus-visible:z-10 focus-visible:ring-1"
onClick={onClick || (() => {})}
>
{external && (
<span className="text-scale-900 group-hover:text-scale-1100 truncate text-sm transition">
<IconArrowUpRight size={'tiny'} />
</span>
)}
<span
title={label}
className="text-scale-1100 group-hover:text-scale-1200 w-full truncate text-sm transition"
>
{isSubitem ? <p>{label}</p> : label}
</span>
</button>
</a>
</Link>
)
}
+2 -2
View File
@@ -17,14 +17,14 @@ const BillingLayout: FC<Props> = ({ children }) => {
<BaseLayout hideHeader hideIconBar>
<div className="flex h-full w-full flex-col">
{/* Header */}
<div className="dark:border-dark flex items-center space-x-3 border-b py-4 px-5">
<div className="dark:border-dark flex items-center space-x-4 border-b py-4 px-5">
<Link href={`/project/${ui.selectedProject?.ref}/settings/billing`} passHref>
<a className="text-scale-900 hover:text-scale-1200 transition-colors">
<IconX size={16} strokeWidth={1.5} />
</a>
</Link>
<div className="flex items-center space-x-6">
<h1 className="text-scale-1200 text-base">Customize your plan</h1>
<h1 className="text-scale-1200 text-sm">Customize your plan</h1>
<div className="bg-scale-600 h-6 w-px"></div>
<div className="flex items-center space-x-3">
<p className="text-scale-1100 text-sm">{ui.selectedOrganization?.name}</p>
@@ -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) {
+1 -11
View File
@@ -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<T>(
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<T>(
}
}, [isLoading, router.isReady, ref, slug])
useEffect(() => {
if (!isLoading && !isRedirecting && router.isReady) {
setConnecting(false)
}
}, [isLoading, isRedirecting, router.isReady])
if (isConnecting) return <Connecting />
return <WrappedComponent {...props} />
}
}
+14 -13
View File
@@ -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<T = unknown>(
}
}
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}`
}
+1 -1
View File
@@ -10,7 +10,7 @@ export async function delete_<T = any>(
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),
+2 -2
View File
@@ -9,7 +9,7 @@ export async function get<T = any>(
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<T = any>(
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',
+2 -2
View File
@@ -10,7 +10,7 @@ export async function head<T = any>(
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<T = any>(
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',
+1 -1
View File
@@ -10,7 +10,7 @@ export async function patch<T = any>(
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),
+1 -1
View File
@@ -10,7 +10,7 @@ export async function post<T = any>(
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),
+1 -1
View File
@@ -10,7 +10,7 @@ export const auth = new GoTrueClient({
export const getAuthUser = async (token: String): Promise<any> => {
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 }
@@ -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) {
+61 -59
View File
@@ -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",
+1 -1
View File
@@ -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",
+6 -13
View File
@@ -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) {
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<link rel="stylesheet" type="text/css" href="/css/fonts.css" />
</Head>
<GoTrueWrapper>
<PageTelemetry>
<RouteValidationWrapper>
<AppBannerWrapper>{getLayout(<Component {...pageProps} />)}</AppBannerWrapper>
</RouteValidationWrapper>
</PageTelemetry>
</GoTrueWrapper>
<PageTelemetry>
<RouteValidationWrapper>
<AppBannerWrapper>{getLayout(<Component {...pageProps} />)}</AppBannerWrapper>
</RouteValidationWrapper>
</PageTelemetry>
<PortalToast />
</FlagProvider>
</StoreProvider>
+7 -4
View File
@@ -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 <Connecting />
}
if (!profile) {
return <UnauthorizedLanding />
@@ -98,6 +100,7 @@ const IndexLayout = withAuth(
} else {
router.push('/')
}
return <Connecting />
}
}
+24 -343
View File
@@ -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(() => {
</section>
<nav className="">
<Tabs onChange={(id: any) => setSelectedTab(id)} type="underlined">
{/* @ts-ignore */}
<Tabs.Panel id="GENERAL" label="General">
<></>
</Tabs.Panel>
{/* @ts-ignore */}
<Tabs.Panel id="TEAM" label="Team">
<></>
</Tabs.Panel>
{/* @ts-ignore */}
<Tabs.Panel id="BILLING" label="Billing">
<></>
</Tabs.Panel>
{/* @ts-ignore */}
<Tabs.Panel id="INVOICES" label="Invoices">
<></>
</Tabs.Panel>
<Tabs.Panel id="GENERAL" label="General" />
<Tabs.Panel id="TEAM" label="Team" />
<Tabs.Panel id="BILLING" label="Billing" />
<Tabs.Panel id="INVOICES" label="Invoices" />
</Tabs>
</nav>
</div>
@@ -289,123 +268,13 @@ const OrgDeletePanel = observer(() => {
<p className="text-red-900">
Make sure you have made a backup if you want to keep your data
</p>
<OrgDeleteModal />
<DeleteOrganizationButton />
</Alert>
</Panel.Content>
</Panel>
)
})
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 (
<>
<div className="mt-2">
<Button onClick={toggle} type="danger">
Delete organization
</Button>
</div>
<Modal
visible={isOpen}
onCancel={toggle}
header={
<div className="flex items-baseline gap-2">
<h5 className="text-scale-1200 text-sm">Delete organisation</h5>
<span className="text-scale-900 text-xs">Are you sure?</span>
</div>
}
size="small"
hideFooter
closable
>
<Form
initialValues={{
orgName: '',
}}
validateOnBlur
onSubmit={async (values: any, { setSubmitting }: any) => {
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 }) => (
<div className="space-y-4 py-3">
<Modal.Content>
<p className="text-scale-900 text-sm">
This action <span className="text-scale-1200">cannot</span> be undone. This will
permanently delete the <span className="text-scale-1200">{orgName}</span>{' '}
organization and remove all of its projects.
</p>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
<Input
id="orgName"
label={
<span>
Please type <Typography.Text strong>{orgSlug}</Typography.Text> to confirm
</span>
}
onChange={(e) => setValue(e.target.value)}
value={value}
placeholder="Type in the orgnaization name"
className="w-full"
/>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
<Button
type="danger"
htmlType="submit"
loading={isSubmitting}
size="small"
block
danger
>
I understand, delete this organization
</Button>
</Modal.Content>
</div>
)}
</Form>
</Modal>
</>
)
})
const TeamSettings = observer(() => {
const PageState: any = useContext(PageContext)
const { ui } = useStore()
@@ -532,8 +401,8 @@ const MembersView = observer(() => {
<Table.td>
{x.invited_id && (
<Badge color={inviteExpired(x.invited_at) ? 'yellow' : 'red'}>
{inviteExpired(x.invited_at) ? 'Invited' : 'Expired'}
<Badge color={isInviteExpired(x.invited_at) ? 'yellow' : 'red'}>
{isInviteExpired(x.invited_at) ? 'Invited' : 'Expired'}
</Badge>
)}
</Table.td>
@@ -572,191 +441,3 @@ const MembersView = observer(() => {
</div>
)
})
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 (
<div className="flex items-center justify-end">
<Dropdown
side="bottom"
align="end"
overlay={
<>
{!member.invited_at && (
<Dropdown.Item onClick={() => setOwnerTransferIsVisble(!ownerTransferIsVisble)}>
<div className="flex flex-col">
<p>Make owner</p>
<p className="block opacity-50">Transfer ownership of "{orgName}"</p>
</div>
</Dropdown.Item>
)}
{member.invited_at && (
<>
<Dropdown.Item onClick={() => handleRevokeInvitation(member.invited_id)}>
<div className="flex flex-col">
<p>Cancel invitation</p>
<p className="block opacity-50">Revoke this invitation.</p>
</div>
</Dropdown.Item>
{!inviteExpired(member.invited_at) && (
<>
<Dropdown.Seperator />
<Dropdown.Item onClick={() => handleResendInvite(member)}>
<div className="flex flex-col">
<p>Resend invitation</p>
<p className="block opacity-50">Invites expire after 24hrs.</p>
</div>
</Dropdown.Item>
</>
)}
</>
)}
{!member.invited_at && (
<>
<Dropdown.Seperator />
<Dropdown.Item icon={<IconTrash size="tiny" />} onClick={handleMemberDelete}>
Remove member
</Dropdown.Item>
</>
)}
</>
}
>
<Button
as="span"
disabled={loading}
loading={loading}
type="text"
icon={<IconMoreHorizontal />}
></Button>
</Dropdown>
<TextConfirmModal
title="Transfer organization"
visible={ownerTransferIsVisble}
confirmString={orgSlug}
loading={loading}
confirmLabel="I understand, transfer ownership"
confirmPlaceholder="Type in name of orgnization"
onCancel={() => 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={
<span>
By transferring this organization, it will be solely owned by{' '}
<span className="font-medium dark:text-white">{member.profile?.username}</span>, they
will also be able to remove you from the organization as a member
</span>
}
/>
</div>
)
})
+3 -1
View File
@@ -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) {
+4 -8
View File
@@ -545,14 +545,10 @@ const PostgrestConfig = observer(({ config, projectRef }: any) => {
}
emptyMessage={
<>
<Typography.Text className="mb-2">
<IconAlertCircle />
</Typography.Text>
<Typography.Text>No schema available to choose</Typography.Text>
<div>
<Typography.Text small className="opacity-50">
New schema you create will appear here
</Typography.Text>
<IconAlertCircle strokeWidth={2} />
<div className="mt-2 flex flex-col text-center">
<p className="text-sm align-center">No schema available to choose</p>
<p className="text-xs opacity-50">New schemas you create will appear here</p>
</div>
</>
}
@@ -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({
@@ -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'