mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Revert "Revert "chore: remove global loading spinner""
This reverts commit c5be38e52a.
This commit is contained in:
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,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>
|
||||
)
|
||||
}
|
||||
@@ -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,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} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Generated
+61
-59
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user