mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
commit
74a6f3732b
20 files changed
+700
-13
No files matched your search
@@ -37,7 +37,7 @@ Spread the word on social channels and help to answer Supabase-related questions
|
||||
|
||||
### Builder
|
||||
|
||||
Build Supabase examples, blog about them, and add them to the [SupaSquad GitHub Org](https://github.com/supasquad).
|
||||
Build Supabase examples, blog about them, and add them to the [Supabase repo](https://github.com/supabase/supabase/tree/master/examples).
|
||||
|
||||
### Author
|
||||
|
||||
@@ -59,7 +59,6 @@ Help us maintain the community guidelines in our GitHub and Community-led commun
|
||||
- Free credits that you can use for Squad efforts.
|
||||
- Direct access to members of the Supabase team for questions, suggestions, etc.
|
||||
- Help shape the future of the program.
|
||||
- Invited to the [SupaSquad GitHub Org](https://github.com/supasquad) to collaborate with the other squad members.
|
||||
- Exclusive Supabase Team swag drops are usually exclusively reserved for the Supabase core team.
|
||||
|
||||
## How to join
|
||||
|
||||
Generated
+4
-4
@@ -42804,7 +42804,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@supabase/gotrue-js": "^2.31.0",
|
||||
"@supabase/gotrue-js": "^2.35.0",
|
||||
"@supabase/ui": "^0.37.0-alpha.50",
|
||||
"react-use": "^17.4.0"
|
||||
},
|
||||
@@ -42817,9 +42817,9 @@
|
||||
}
|
||||
},
|
||||
"packages/common/node_modules/@supabase/gotrue-js": {
|
||||
"version": "2.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.31.0.tgz",
|
||||
"integrity": "sha512-YcwlbbNfedlue/HVIXtYBb4fuOrs29gNOTl6AmyxPp4zryRxzFvslVN9kmLDBRUAVU9fnPJh2bgOR3chRjJX5w==",
|
||||
"version": "2.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.35.0.tgz",
|
||||
"integrity": "sha512-bLNIrBzFNRR61qUN8VZ4iV7q+fPilWyYQi0CZlxuHBiPtVcNPTul02DbNvgvw76hNz+mB5maay/Y17lSHnSRmw==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { GoTrueClient } from '@supabase/gotrue-js'
|
||||
|
||||
export const STORAGE_KEY = process.env.NEXT_PUBLIC_STORAGE_KEY || 'supabase.dashboard.auth.token'
|
||||
export const AUTH_DEBUG_KEY =
|
||||
process.env.NEXT_PUBLIC_AUTH_DEBUG_KEY || 'supabase.dashboard.auth.debug'
|
||||
|
||||
const debug =
|
||||
process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' &&
|
||||
globalThis &&
|
||||
globalThis.localStorage &&
|
||||
globalThis.localStorage.getItem(AUTH_DEBUG_KEY) === 'true'
|
||||
|
||||
export const gotrueClient = new GoTrueClient({
|
||||
url: process.env.NEXT_PUBLIC_GOTRUE_URL,
|
||||
storageKey: STORAGE_KEY,
|
||||
detectSessionInUrl: true,
|
||||
debug,
|
||||
})
|
||||
@@ -6,7 +6,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@supabase/gotrue-js": "^2.31.0",
|
||||
"@supabase/gotrue-js": "^2.35.0",
|
||||
"@supabase/ui": "^0.37.0-alpha.50",
|
||||
"react-use": "^17.4.0"
|
||||
},
|
||||
|
||||
@@ -54,6 +54,9 @@ const Tabs: React.FC<TabsProps> & TabsSubComponents = ({
|
||||
children = [children]
|
||||
}
|
||||
|
||||
// Exclude children that are just conditional elements
|
||||
children = children.filter((it) => it)
|
||||
|
||||
const tabIds = children.map((tab) => tab.props.id)
|
||||
|
||||
const { groupActiveId, setGroupActiveId } = useTabGroup(tabIds)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
clispec: '001'
|
||||
info:
|
||||
id: cli
|
||||
version: 1.74.0
|
||||
version: 1.75.1
|
||||
title: Supabase CLI
|
||||
language: sh
|
||||
source: https://github.com/supabase/cli
|
||||
@@ -994,6 +994,7 @@ commands:
|
||||
- management-api
|
||||
links: []
|
||||
subcommands:
|
||||
- supabase-orgs-create
|
||||
- supabase-orgs-list
|
||||
flags: []
|
||||
- id: supabase-orgs-list
|
||||
@@ -1005,6 +1006,15 @@ commands:
|
||||
usage: supabase orgs list
|
||||
subcommands: []
|
||||
flags: []
|
||||
- id: supabase-orgs-create
|
||||
title: supabase orgs create
|
||||
summary: Create an organization
|
||||
description: Create an organization for the logged-in user.
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase orgs create
|
||||
subcommands: []
|
||||
flags: []
|
||||
- id: supabase-network-restrictions
|
||||
title: supabase network-restrictions
|
||||
summary: Manage network restrictions
|
||||
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
IconAlertCircle,
|
||||
IconLoader,
|
||||
IconShield,
|
||||
IconTool,
|
||||
IconUsers,
|
||||
Listbox,
|
||||
Loading,
|
||||
Modal,
|
||||
} from 'ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore, useSelectedProject, useCheckPermissions } from 'hooks'
|
||||
import { useProjectTransferPreviewQuery } from 'data/projects/project-transfer-preview-query'
|
||||
import { useProjectTransferMutation } from 'data/projects/project-transfer-mutation'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import Link from 'next/link'
|
||||
|
||||
const TransferProjectButton: FC<{}> = () => {
|
||||
const { ui } = useStore()
|
||||
|
||||
const project = useSelectedProject()
|
||||
const projectRef = project?.ref
|
||||
const projectOrgId = project?.organization_id
|
||||
const { data: allOrganizations } = useOrganizationsQuery()
|
||||
|
||||
const organizations = (allOrganizations || [])
|
||||
.filter((it) => it.id !== projectOrgId)
|
||||
// Only orgs with org-level subscription
|
||||
.filter((it) => it.subscription_id)
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const [selectedOrg, setSelectedOrg] = useState()
|
||||
|
||||
const { error: transferError, mutateAsync: transferProject } = useProjectTransferMutation()
|
||||
|
||||
const {
|
||||
data: transferPreviewData,
|
||||
error: transferPreviewError,
|
||||
isLoading: transferPreviewIsLoading,
|
||||
remove,
|
||||
} = useProjectTransferPreviewQuery(
|
||||
{ projectRef, targetOrganizationSlug: selectedOrg },
|
||||
{ enabled: !isLoading && isOpen }
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// reset state
|
||||
setSelectedOrg(undefined)
|
||||
setIsLoading(false)
|
||||
} else {
|
||||
// Invalidate cache
|
||||
remove()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const canTransferProject = useCheckPermissions(PermissionAction.UPDATE, 'projects')
|
||||
|
||||
const toggle = () => {
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
async function handleTransferProject() {
|
||||
if (project === undefined) return
|
||||
if (selectedOrg === undefined) return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
await transferProject({ projectRef, targetOrganizationSlug: selectedOrg })
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
|
||||
duration: 5000,
|
||||
message: `Successfully transferred project ${project?.name}.`,
|
||||
})
|
||||
setIsOpen(false)
|
||||
} catch {
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button onClick={toggle} type="default" disabled={!canTransferProject}>
|
||||
Transfer project
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canTransferProject && (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to delete this project
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
|
||||
<Modal
|
||||
closable
|
||||
onCancel={() => toggle()}
|
||||
visible={isOpen}
|
||||
loading={isLoading}
|
||||
size={'xlarge'}
|
||||
header={`Transfer project ${project?.name}`}
|
||||
customFooter={
|
||||
<div className="flex items-center space-x-2 justify-end">
|
||||
<Button type="default" onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleTransferProject()}
|
||||
disabled={
|
||||
!transferPreviewData || !transferPreviewData.valid || isLoading || !selectedOrg
|
||||
}
|
||||
>
|
||||
Transfer Project
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4 py-4 text-scale-1100">
|
||||
<Modal.Content>
|
||||
<p className="text-sm">
|
||||
To transfer projects, the owner must be a member of both the source and target
|
||||
organizations.
|
||||
</p>
|
||||
|
||||
<p className="font-bold mt-6 text-sm">Transferring considerations:</p>
|
||||
|
||||
<ul className="mt-4 space-y-5 text-sm px-4">
|
||||
<li className="flex gap-4">
|
||||
<span className="shrink-0 mt-1">
|
||||
<IconLoader />
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-bold">No downtime</p>
|
||||
<p>There is no downtime or restrictions involved when transferring a project.</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li className="flex gap-4">
|
||||
<span className="shrink-0 mt-1">
|
||||
<IconShield />
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-bold">Permissions</p>
|
||||
<p>
|
||||
Depending on your role in the target organization, your level of permissions may
|
||||
change after transfer.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li className="flex gap-4">
|
||||
<span className="shrink-0 mt-1">
|
||||
<IconTool w={14} className="flex-shrink-0" />
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-bold">Features</p>
|
||||
<p>
|
||||
Moving your project to an organization with a smaller subscription plan may
|
||||
result in the loss of certain features (i.e. image transformations).
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</Modal.Content>
|
||||
<Modal.Content>
|
||||
{organizations && (
|
||||
<div className="mt-8 mx-4 border-t pt-4 space-y-2">
|
||||
{organizations.length === 0 ? (
|
||||
<div className="flex items-center gap-2 bg-scale-400 p-3 text-sm">
|
||||
<IconAlertCircle /> You do not have any organizations with an organization-level
|
||||
subscription.
|
||||
</div>
|
||||
) : (
|
||||
<Listbox
|
||||
label="Select Target Organization"
|
||||
layout="vertical"
|
||||
value={selectedOrg}
|
||||
onChange={(slug) => setSelectedOrg(slug)}
|
||||
placeholder="Select Organization"
|
||||
>
|
||||
<Listbox.Option disabled key="no-results" label="Select Organization" value="">
|
||||
Select Organization
|
||||
</Listbox.Option>
|
||||
{organizations.map((x: any) => (
|
||||
<Listbox.Option
|
||||
key={x.id}
|
||||
label={x.name}
|
||||
value={x.slug}
|
||||
addOnBefore={() => <IconUsers />}
|
||||
>
|
||||
{x.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
)}
|
||||
|
||||
<p className="text-scale-1000 text-sm">
|
||||
The target organization needs to use{' '}
|
||||
<Link href="https://www.notion.so/supabase/Organization-Level-Billing-9c159d69375b4af095f0b67881276582?pvs=4">
|
||||
<a target="_blank" rel="noreferrer" className="underline">
|
||||
organization-level-billing
|
||||
</a>
|
||||
</Link>
|
||||
. To migrate an organization to the new billing, head to your{' '}
|
||||
<Link href="/org/_/general">
|
||||
<a target="_blank" rel="noreferrer" className="underline">
|
||||
organizations settings
|
||||
</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Modal.Content>
|
||||
|
||||
<Loading active={selectedOrg !== undefined && transferPreviewIsLoading}>
|
||||
<Modal.Content>
|
||||
<div className="px-4">
|
||||
{transferPreviewData && transferPreviewData.valid && (
|
||||
<div className="text-sm text-scale-1100 p-4 bg-scale-400">
|
||||
{transferPreviewData.source_subscription_plan !==
|
||||
transferPreviewData.target_subscription_plan ? (
|
||||
<div>
|
||||
<p>
|
||||
Your project is currently on the{' '}
|
||||
{transferPreviewData.source_subscription_plan} plan, whereas the target
|
||||
organization uses the {transferPreviewData.target_subscription_plan} plan.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
Your project and the target organization are both on the{' '}
|
||||
{transferPreviewData.source_subscription_plan} subscription plan.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="my-4">
|
||||
{transferPreviewData.credits_on_source_organization === 0 ? (
|
||||
<span>
|
||||
{' '}
|
||||
Your current organization won't be granted any prorated credits.
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{' '}
|
||||
Your current organization will be granted{' '}
|
||||
<span className="text-brand-900">
|
||||
${transferPreviewData.credits_on_source_organization}
|
||||
</span>{' '}
|
||||
in credits as proration.
|
||||
</span>
|
||||
)}
|
||||
{transferPreviewData.costs_on_target_organization === 0 ? (
|
||||
<span>
|
||||
{' '}
|
||||
The target organization won't be charged any immediate upfront payment.
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{' '}
|
||||
The target organization will be billed{' '}
|
||||
<span className="text-brand-900">
|
||||
${transferPreviewData.costs_on_target_organization}
|
||||
</span>{' '}
|
||||
immediately to prorate for the remainder of the billing period.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{transferPreviewData && transferPreviewData.warnings.length > 0 && (
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
title="Warnings for project transfer"
|
||||
className="mt-3"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{transferPreviewData.warnings.map((warning) => (
|
||||
<p key={warning.key}>{warning.message}</p>
|
||||
))}
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
{transferPreviewData && transferPreviewData.errors.length > 0 && (
|
||||
<Alert withIcon variant="danger" title="Project cannot be transferred">
|
||||
<div className="space-y-1">
|
||||
{transferPreviewData.errors.map((error) => (
|
||||
<p key={error.key}>{error.message}</p>
|
||||
))}
|
||||
</div>
|
||||
{transferPreviewData.members_exceeding_free_project_limit.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-scale-1100">
|
||||
These members have reached their maximum limits for the number of active
|
||||
Free plan projects within organizations where they are an administrator or
|
||||
owner:
|
||||
</p>
|
||||
<ul className="pl-5 text-sm list-disc text-scale-1100">
|
||||
{(transferPreviewData.members_exceeding_free_project_limit || []).map(
|
||||
(member, idx: number) => (
|
||||
<li key={`member-${idx}`}>
|
||||
{member.name} (Limit: {member.limit} free projects)
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
<p className="text-sm text-scale-1100">
|
||||
These members will need to either delete, pause, or upgrade one or more of
|
||||
their projects before you can downgrade this project.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
{transferPreviewError && !transferError && (
|
||||
<Alert withIcon variant="danger" title="Project cannot be transferred">
|
||||
<p>{transferPreviewError.message}</p>
|
||||
</Alert>
|
||||
)}
|
||||
{transferError && (
|
||||
<Alert withIcon variant="danger" title="Project cannot be transferred">
|
||||
<p>{transferError.message}</p>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</Loading>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransferProjectButton
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
import { useSelectedProject } from 'hooks'
|
||||
import Panel from 'components/ui/Panel'
|
||||
|
||||
import TransferProjectButton from './TransferProjectButton'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
import { IconTruck } from 'ui'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const TransferProjectPanel: FC<Props> = ({}) => {
|
||||
const project = useSelectedProject()
|
||||
|
||||
if (project === undefined) return <></>
|
||||
|
||||
return (
|
||||
<section>
|
||||
<FormHeader
|
||||
title="Transfer Project"
|
||||
description="Transfer your project to a different organization with no downtime."
|
||||
/>
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="flex justify-between items-center gap-8">
|
||||
<div className="flex space-x-4">
|
||||
<IconTruck className="mt-1" />
|
||||
<div className="space-y-1 xl:max-w-lg">
|
||||
<p className="text-sm">Transfer project to another organization</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
To transfer projects, the owner must be a member of both the source and target
|
||||
organizations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<TransferProjectButton />
|
||||
</div>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransferProjectPanel
|
||||
@@ -3,5 +3,15 @@ import Infrastructure from './Infrastructure/Infrastructure'
|
||||
import CustomDomainConfig from './CustomDomainConfig/CustomDomainConfig'
|
||||
import DeleteProjectPanel from './DeleteProjectPanel/DeleteProjectPanel'
|
||||
import DeleteProjectButton from './DeleteProjectPanel/DeleteProjectButton'
|
||||
import TransferProjectButton from './TransferProjectPanel/TransferProjectButton'
|
||||
import TransferProjectPanel from './TransferProjectPanel/TransferProjectPanel'
|
||||
|
||||
export { General, Infrastructure, CustomDomainConfig, DeleteProjectPanel, DeleteProjectButton }
|
||||
export {
|
||||
General,
|
||||
Infrastructure,
|
||||
CustomDomainConfig,
|
||||
DeleteProjectPanel,
|
||||
DeleteProjectButton,
|
||||
TransferProjectPanel,
|
||||
TransferProjectButton,
|
||||
}
|
||||
@@ -26,7 +26,6 @@ const UpgradeToPro: FC<Props> = ({ icon, primaryText, projectRef, secondaryText
|
||||
'stripe.subscriptions'
|
||||
)
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
const isEnterprise = tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -48,11 +47,11 @@ const UpgradeToPro: FC<Props> = ({ icon, primaryText, projectRef, secondaryText
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button type="primary" disabled={!canUpdateSubscription || projectUpdateDisabled}>
|
||||
<Link href={`/project/${ref}/settings/billing/subscription?panel=subscriptionPlan`}>
|
||||
<Link href={`/project/${ref}/settings/billing/subscription?panel=${tier === PRICING_TIER_PRODUCT_IDS.FREE ? 'subscriptionPlan' : 'customDomain'}`}>
|
||||
<a>
|
||||
{tier === PRICING_TIER_PRODUCT_IDS.FREE
|
||||
? 'Upgrade to Pro'
|
||||
: 'Modify subscription'}
|
||||
: 'Enable Custom Domain'}
|
||||
</a>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export const projectKeys = {
|
||||
list: () => ['all-projects'] as const,
|
||||
detail: (projectRef: string | undefined) => ['project', projectRef, 'detail'] as const,
|
||||
projectTransfer: (projectRef: string | undefined, targetOrganizationSlug: string | undefined) =>
|
||||
['projects', 'transfer', projectRef, targetOrganizationSlug] as const,
|
||||
projectTransferPreview: (
|
||||
projectRef: string | undefined,
|
||||
targetOrganizationSlug: string | undefined
|
||||
) => ['projects', 'transfer', projectRef, targetOrganizationSlug, 'preview'] as const,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { projectKeys } from './keys'
|
||||
|
||||
export type ProjectTransferVariables = {
|
||||
projectRef?: string
|
||||
targetOrganizationSlug?: string
|
||||
}
|
||||
|
||||
type ProjectTransferError = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export async function transferProject({
|
||||
projectRef,
|
||||
targetOrganizationSlug,
|
||||
}: ProjectTransferVariables) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
if (!targetOrganizationSlug) throw new Error('targetOrganizationSlug is required')
|
||||
|
||||
const payload: { target_organization_slug: string } = {
|
||||
target_organization_slug: targetOrganizationSlug,
|
||||
}
|
||||
|
||||
const response = await post(`${API_URL}/projects/${projectRef}/transfer`, payload)
|
||||
if (response.error) throw response.error
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type ProjectTransferData = Awaited<ReturnType<typeof transferProject>>
|
||||
|
||||
export const useProjectTransferMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<ProjectTransferData, ProjectTransferError, ProjectTransferVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<ProjectTransferData, ProjectTransferError, ProjectTransferVariables>(
|
||||
(vars) => transferProject(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef, targetOrganizationSlug } = variables
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(
|
||||
projectKeys.projectTransferPreview(projectRef, targetOrganizationSlug)
|
||||
),
|
||||
queryClient.invalidateQueries(projectKeys.detail(projectRef)),
|
||||
queryClient.invalidateQueries(projectKeys.list()),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { projectKeys } from './keys'
|
||||
|
||||
export type ProjectTransferPreviewVariables = {
|
||||
projectRef?: string
|
||||
targetOrganizationSlug?: string
|
||||
}
|
||||
|
||||
export type PlanId = 'free' | 'pro' | 'team' | 'enterprise'
|
||||
|
||||
type MemberExceedingFreeProjectLimit = {
|
||||
name: string
|
||||
limit: number
|
||||
}
|
||||
|
||||
type PreviewTransferInfo = {
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export type PreviewProjectTransferResponse = {
|
||||
valid: boolean
|
||||
|
||||
warnings: PreviewTransferInfo[]
|
||||
errors: PreviewTransferInfo[]
|
||||
|
||||
members_exceeding_free_project_limit: MemberExceedingFreeProjectLimit[]
|
||||
|
||||
has_permissions_on_source_organization: boolean
|
||||
has_access_to_target_organization: boolean
|
||||
|
||||
source_project_eligible: boolean
|
||||
|
||||
target_organization_eligible: boolean | null
|
||||
target_organization_has_free_project_slots: boolean | null
|
||||
|
||||
credits_on_source_organization: number
|
||||
costs_on_target_organization: number
|
||||
charge_on_target_organization: number
|
||||
|
||||
source_subscription_plan: PlanId
|
||||
target_subscription_plan: PlanId | null
|
||||
}
|
||||
|
||||
export async function previewProjectTransfer(
|
||||
{ projectRef, targetOrganizationSlug }: ProjectTransferPreviewVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
if (!targetOrganizationSlug) throw new Error('targetOrganizationSlug is required')
|
||||
|
||||
const response = await post(
|
||||
`${API_URL}/projects/${projectRef}/transfer/preview`,
|
||||
{
|
||||
target_organization_slug: targetOrganizationSlug,
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
if (response.error) throw response.error
|
||||
|
||||
return response as PreviewProjectTransferResponse
|
||||
}
|
||||
|
||||
export type ProjectTransferPreviewData = Awaited<ReturnType<typeof previewProjectTransfer>>
|
||||
export type ProjectTransferPreviewError = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export const useProjectTransferPreviewQuery = <TData = ProjectTransferPreviewData>(
|
||||
{ projectRef, targetOrganizationSlug }: ProjectTransferPreviewVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ProjectTransferPreviewData, ProjectTransferPreviewError, TData> = {}
|
||||
) =>
|
||||
useQuery<ProjectTransferPreviewData, ProjectTransferPreviewError, TData>(
|
||||
projectKeys.projectTransferPreview(projectRef, targetOrganizationSlug),
|
||||
({ signal }) => previewProjectTransfer({ projectRef, targetOrganizationSlug }, signal),
|
||||
{
|
||||
enabled:
|
||||
enabled &&
|
||||
typeof projectRef !== 'undefined' &&
|
||||
typeof targetOrganizationSlug !== 'undefined',
|
||||
...options,
|
||||
|
||||
retry: (failureCount, error) => {
|
||||
// Don't retry on 400s
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'code' in error &&
|
||||
(error as any).code === 400
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (failureCount < 3) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export const useProjectTransferPreviewPrefetch = ({
|
||||
projectRef,
|
||||
targetOrganizationSlug,
|
||||
}: ProjectTransferPreviewVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef && targetOrganizationSlug) {
|
||||
client.prefetchQuery(
|
||||
projectKeys.projectTransferPreview(projectRef, targetOrganizationSlug),
|
||||
({ signal }) => previewProjectTransfer({ projectRef, targetOrganizationSlug }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef, targetOrganizationSlug])
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export const FLY_REGIONS = {
|
||||
} as const
|
||||
|
||||
export const AWS_REGIONS_DEFAULT =
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' ? AWS_REGIONS.SOUTHEAST_ASIA : AWS_REGIONS.EAST_US
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' ? AWS_REGIONS.SOUTHEAST_ASIA : AWS_REGIONS.WEST_US
|
||||
|
||||
// TO DO, change default to US region for prod
|
||||
export const FLY_REGIONS_DEFAULT = FLY_REGIONS.SOUTHEAST_ASIA
|
||||
|
||||
@@ -5,6 +5,7 @@ export const LOCAL_STORAGE_KEYS_ALLOWLIST = [
|
||||
'theme',
|
||||
'supabaseDarkMode',
|
||||
'supabase.dashboard.sign_in_clicks',
|
||||
'supabase.dashboard.auth.debug',
|
||||
]
|
||||
|
||||
export function clearLocalStorage() {
|
||||
|
||||
@@ -74,6 +74,9 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
EXTERNAL_GOOGLE_ENABLED: false,
|
||||
EXTERNAL_GOOGLE_CLIENT_ID: null,
|
||||
EXTERNAL_GOOGLE_SECRET: null,
|
||||
EXTERNAL_KAKAO_ENABLED: false,
|
||||
EXTERNAL_KAKAO_CLIENT_ID: null,
|
||||
EXTERNAL_KAKAO_SECRET: null,
|
||||
EXTERNAL_KEYCLOAK_ENABLED: false,
|
||||
EXTERNAL_KEYCLOAK_CLIENT_ID: null,
|
||||
EXTERNAL_KEYCLOAK_SECRET: null,
|
||||
|
||||
@@ -7,15 +7,20 @@ import {
|
||||
Infrastructure,
|
||||
CustomDomainConfig,
|
||||
DeleteProjectPanel,
|
||||
TransferProjectPanel,
|
||||
} from 'components/interfaces/Settings/General'
|
||||
import { useFlag } from 'hooks'
|
||||
|
||||
const ProjectSettings: NextPageWithLayout = () => {
|
||||
const transferProjectEnabled = useFlag('transferProject')
|
||||
|
||||
// [Joshen] Opting for larger gap instead of gap-8 as compared to other pages for better grouping of content
|
||||
return (
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32 ">
|
||||
<General />
|
||||
<Infrastructure />
|
||||
<CustomDomainConfig />
|
||||
{transferProjectEnabled && <TransferProjectPanel />}
|
||||
<DeleteProjectPanel />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0 0 256 256"><path fill="#FFE812" d="M256 236c0 11.046-8.954 20-20 20H20c-11.046 0-20-8.954-20-20V20C0 8.954 8.954 0 20 0h216c11.046 0 20 8.954 20 20v216z"/><path d="M128 36C70.562 36 24 72.713 24 118c0 29.279 19.466 54.97 48.748 69.477-1.593 5.494-10.237 35.344-10.581 37.689 0 0-.207 1.762.934 2.434s2.483.15 2.483.15c3.272-.457 37.943-24.811 43.944-29.04 5.995.849 12.168 1.29 18.472 1.29 57.438 0 104-36.712 104-82 0-45.287-46.562-82-104-82z"/><path fill="#FFE812" d="M70.5 146.625c-3.309 0-6-2.57-6-5.73V105.25h-9.362c-3.247 0-5.888-2.636-5.888-5.875s2.642-5.875 5.888-5.875h30.724c3.247 0 5.888 2.636 5.888 5.875s-2.642 5.875-5.888 5.875H76.5v35.645c0 3.16-2.691 5.73-6 5.73zM123.112 146.547c-2.502 0-4.416-1.016-4.993-2.65l-2.971-7.778-18.296-.001-2.973 7.783c-.575 1.631-2.488 2.646-4.99 2.646a9.155 9.155 0 0 1-3.814-.828c-1.654-.763-3.244-2.861-1.422-8.52l14.352-37.776c1.011-2.873 4.082-5.833 7.99-5.922 3.919.088 6.99 3.049 8.003 5.928l14.346 37.759c1.826 5.672.236 7.771-1.418 8.532a9.176 9.176 0 0 1-3.814.827c-.001 0 0 0 0 0zm-11.119-21.056L106 108.466l-5.993 17.025h11.986zM138 145.75c-3.171 0-5.75-2.468-5.75-5.5V99.5c0-3.309 2.748-6 6.125-6s6.125 2.691 6.125 6v35.25h12.75c3.171 0 5.75 2.468 5.75 5.5s-2.579 5.5-5.75 5.5H138zM171.334 146.547c-3.309 0-6-2.691-6-6V99.5c0-3.309 2.691-6 6-6s6 2.691 6 6v12.896l16.74-16.74c.861-.861 2.044-1.335 3.328-1.335 1.498 0 3.002.646 4.129 1.772 1.051 1.05 1.678 2.401 1.764 3.804.087 1.415-.384 2.712-1.324 3.653l-13.673 13.671 14.769 19.566a5.951 5.951 0 0 1 1.152 4.445 5.956 5.956 0 0 1-2.328 3.957 5.94 5.94 0 0 1-3.609 1.211 5.953 5.953 0 0 1-4.793-2.385l-14.071-18.644-2.082 2.082v13.091a6.01 6.01 0 0 1-6.002 6.003z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -784,6 +784,46 @@ const EXTERNAL_PROVIDER_GOOGLE = {
|
||||
},
|
||||
}
|
||||
|
||||
const EXTERNAL_PROVIDER_KAKAO = {
|
||||
$schema: JSON_SCHEMA_VERSION,
|
||||
type: 'object',
|
||||
title: 'Kakao',
|
||||
properties: {
|
||||
EXTERNAL_KAKAO_ENABLED: {
|
||||
title: 'Kakao enabled',
|
||||
type: 'boolean',
|
||||
},
|
||||
// [TODO] Update docs
|
||||
EXTERNAL_KAKAO_CLIENT_ID: {
|
||||
title: 'REST API Key',
|
||||
type: 'string',
|
||||
},
|
||||
// [TODO] Update docs
|
||||
EXTERNAL_KAKAO_SECRET: {
|
||||
title: 'Client Secret Code',
|
||||
type: 'string',
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
validationSchema: object().shape({
|
||||
EXTERNAL_KAKAO_ENABLED: boolean().required(),
|
||||
EXTERNAL_KAKAO_CLIENT_ID: string().when('EXTERNAL_KAKAO_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema.required('REST API Key is required'),
|
||||
otherwise: (schema) => schema,
|
||||
}),
|
||||
EXTERNAL_KAKAO_SECRET: string().when('EXTERNAL_KAKAO_ENABLED', {
|
||||
is: true,
|
||||
then: (schema) => schema.required('Client Secret Code is required'),
|
||||
otherwise: (schema) => schema,
|
||||
}),
|
||||
}),
|
||||
misc: {
|
||||
iconKey: 'kakao-icon',
|
||||
requiresRedirect: true,
|
||||
},
|
||||
}
|
||||
|
||||
// [TODO]: clarify the EXTERNAL_KEYCLOAK_URL property
|
||||
const EXTERNAL_PROVIDER_KEYCLOAK = {
|
||||
$schema: JSON_SCHEMA_VERSION,
|
||||
@@ -1183,6 +1223,7 @@ export const PROVIDERS_SCHEMAS = [
|
||||
EXTERNAL_PROVIDER_GITHUB,
|
||||
EXTERNAL_PROVIDER_GITLAB,
|
||||
EXTERNAL_PROVIDER_GOOGLE,
|
||||
EXTERNAL_PROVIDER_KAKAO,
|
||||
EXTERNAL_PROVIDER_KEYCLOAK,
|
||||
EXTERNAL_PROVIDER_LINKEDIN,
|
||||
EXTERNAL_PROVIDER_NOTION,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"EXTERNAL_GITHUB_ENABLED",
|
||||
"EXTERNAL_GITLAB_ENABLED",
|
||||
"EXTERNAL_GOOGLE_ENABLED",
|
||||
"EXTERNAL_KAKAO_ENABLED",
|
||||
"EXTERNAL_KEYCLOAK_ENABLED",
|
||||
"EXTERNAL_LINKEDIN_ENABLED",
|
||||
"EXTERNAL_NOTION_ENABLED",
|
||||
@@ -196,6 +197,18 @@
|
||||
"title": "Google secret",
|
||||
"type": "string"
|
||||
},
|
||||
"EXTERNAL_KAKAO_ENABLED": {
|
||||
"title": "Kakao enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"EXTERNAL_KAKAO_CLIENT_ID": {
|
||||
"title": "Kakao client ID",
|
||||
"type": "string"
|
||||
},
|
||||
"EXTERNAL_KAKAO_SECRET": {
|
||||
"title": "Kakao secret",
|
||||
"type": "string"
|
||||
},
|
||||
"EXTERNAL_KEYCLOAK_ENABLED": {
|
||||
"title": "Keycloak enabled",
|
||||
"type": "boolean"
|
||||
|
||||
Reference in new issue
Block a user