mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
Merge branch 'master' into feature/expo-social-auth
This commit is contained in:
126 files changed
+2151
-1027
No files matched your search
+1
-1
@@ -203,7 +203,7 @@ Some things to consider:
|
||||
|
||||
## Community channels
|
||||
|
||||
If you get stuck somewhere or have any questions, join our [Discord Community Server](https://discord.supabase.com/) or the [Github Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
|
||||
If you get stuck somewhere or have any questions, join our [Discord Community Server](https://discord.supabase.com/) or the [GitHub Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onPress={performOAuth} title="Sign in with Github" />
|
||||
<Button onPress={performOAuth} title="Sign in with GitHub" />
|
||||
<Button onPress={sendMagicLink} title="Send Magic Link" />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ func signInWithGithub() async throws {
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with `Github` as the `Provider`:
|
||||
When your user signs in, call [signInWith(Provider)](/docs/reference/kotlin/auth-signinwithoauth) with [`Github`](https://github.com/supabase-community/supabase-kt/blob/master/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/Providers.kt#L16-L20) as the `Provider`:
|
||||
|
||||
```kotlin
|
||||
suspend fun signInWithGithub() {
|
||||
|
||||
@@ -60,6 +60,10 @@ If a new version of an extension becomes available on Supabase, you need to init
|
||||
|
||||
### Full list of extensions
|
||||
|
||||
Supabase is pre-configured with over 50 extensions. You can also install your own SQL extensions directly in the database through our SQL editor.
|
||||
Supabase is pre-configured with over 50 extensions and you can install additional extensions through the [database.dev](https://database.dev/) package manager.
|
||||
|
||||
You can install pure SQL extensions directly in the database using the SQL editor or any Postgres client.
|
||||
|
||||
If you would like to request an extension, add (or upvote) it in the [GitHub Discussion](https://github.com/orgs/supabase/discussions/33754).
|
||||
|
||||
<Extensions />
|
||||
@@ -94,7 +94,7 @@ Check out the [Edge Function Examples](https://github.com/supabase/supabase/tree
|
||||
href: '/guides/functions/connect-to-postgres',
|
||||
},
|
||||
{
|
||||
name: 'Github Actions',
|
||||
name: 'GitHub Actions',
|
||||
description: `Deploying Edge Functions with GitHub Actions.`,
|
||||
href: '/guides/functions/examples/github-actions',
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title = "Converted Github account to organization - Lost Supabase account access"
|
||||
title = "Converted GitHub account to organization - Lost Supabase account access"
|
||||
github_url = "https://github.com/orgs/supabase/discussions/21101"
|
||||
date_created = "2024-02-08T01:39:25+00:00"
|
||||
topics = [ "auth" ]
|
||||
|
||||
@@ -3125,7 +3125,7 @@ functions:
|
||||
- name: provider
|
||||
isOptional: false
|
||||
type: OAuthProvider
|
||||
description: The OAuth provider to use for the user's authentication, for example `Google` or `Github`.
|
||||
description: The OAuth provider to use for the user's authentication, for example `Google` or `GitHub`.
|
||||
- name: redirectUrl
|
||||
isOptional: true
|
||||
type: String?
|
||||
|
||||
@@ -3192,7 +3192,7 @@ functions:
|
||||
- name: provider
|
||||
isOptional: false
|
||||
type: OAuthProvider
|
||||
description: The OAuth provider to use for the user's authentication, for example `Google` or `Github`.
|
||||
description: The OAuth provider to use for the user's authentication, for example `Google` or `GitHub`.
|
||||
- name: redirectUrl
|
||||
isOptional: true
|
||||
type: String?
|
||||
|
||||
@@ -149,6 +149,7 @@ const Pagination = () => {
|
||||
<>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
aria-label="Previous page"
|
||||
icon={<ArrowLeft />}
|
||||
type="outline"
|
||||
className="px-1.5"
|
||||
@@ -179,6 +180,7 @@ const Pagination = () => {
|
||||
<p className="text-xs text-foreground-light">of {totalPages.toLocaleString()}</p>
|
||||
|
||||
<Button
|
||||
aria-label="Next page"
|
||||
icon={<ArrowRight />}
|
||||
type="outline"
|
||||
className="px-1.5"
|
||||
|
||||
@@ -129,7 +129,7 @@ const FeaturePreviewModal = () => {
|
||||
<div className="mt-1 mb-3 flex flex-col items-center gap-y-0.5">
|
||||
<p className="text-sm">No feature previews available</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Have an idea for the dashboard? Let us know via Github Discussions!
|
||||
Have an idea for the dashboard? Let us know via GitHub Discussions!
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild type="default" icon={<ExternalLink strokeWidth={1.5} />}>
|
||||
@@ -138,7 +138,7 @@ const FeaturePreviewModal = () => {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Github Discussions
|
||||
GitHub Discussions
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useUserSendOTPMutation } from 'data/auth/user-send-otp-mutation'
|
||||
import { useUserUpdateMutation } from 'data/auth/user-update-mutation'
|
||||
import { User } from 'data/auth/users-infinite-query'
|
||||
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { timeout } from 'lib/helpers'
|
||||
import { Button, cn, Separator } from 'ui'
|
||||
@@ -45,6 +46,10 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
|
||||
const isPhoneAuth = user.phone !== null
|
||||
const isBanned = user.banned_until !== null
|
||||
|
||||
const { authenticationSignInProviders } = useIsFeatureEnabled([
|
||||
'authentication:sign_in_providers',
|
||||
])
|
||||
|
||||
const providers = ((user.raw_app_meta_data?.providers as string[]) ?? []).map(
|
||||
(provider: string) => {
|
||||
return {
|
||||
@@ -238,13 +243,15 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
|
||||
Signed in with a {providerName} account via{' '}
|
||||
{providerName === 'SAML' ? 'SSO' : 'OAuth'}
|
||||
</p>
|
||||
<Button asChild type="default" className="mt-2">
|
||||
<Link
|
||||
href={`/project/${projectRef}/auth/providers?provider=${provider.name === 'SAML' ? 'SAML 2.0' : provider.name}`}
|
||||
>
|
||||
Configure {providerName} provider
|
||||
</Link>
|
||||
</Button>
|
||||
{authenticationSignInProviders && (
|
||||
<Button asChild type="default" className="mt-2">
|
||||
<Link
|
||||
href={`/project/${projectRef}/auth/providers?provider=${provider.name === 'SAML' ? 'SAML 2.0' : provider.name}`}
|
||||
>
|
||||
Configure {providerName} provider
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{isActive ? (
|
||||
<div className="flex items-center gap-1 rounded-full border border-brand-400 bg-brand-200 py-1 px-1 text-xs text-brand">
|
||||
|
||||
@@ -64,6 +64,10 @@ export type ConnectionType = {
|
||||
label: string
|
||||
guideLink?: string
|
||||
children: ConnectionType[]
|
||||
files?: {
|
||||
name: string
|
||||
content: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export const FRAMEWORKS: ConnectionType[] = [
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
|
||||
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
|
||||
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
@@ -30,20 +31,31 @@ import {
|
||||
} from 'ui'
|
||||
import { CONNECTION_TYPES, ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './Connect.constants'
|
||||
import { getContentFilePath } from './Connect.utils'
|
||||
import ConnectDropdown from './ConnectDropdown'
|
||||
import ConnectTabContent from './ConnectTabContent'
|
||||
import { ConnectDropdown } from './ConnectDropdown'
|
||||
import { ConnectTabContent } from './ConnectTabContent'
|
||||
import { ConnectTabContentCustom } from './ConnectTabContentCustom'
|
||||
|
||||
export const Connect = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: selectedProject } = useSelectedProjectQuery()
|
||||
const isActiveHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY
|
||||
|
||||
const { connectFrameworks } = useCustomContent(['connect:frameworks'])
|
||||
const connectionTypes = !connectFrameworks
|
||||
? CONNECTION_TYPES
|
||||
: [
|
||||
{ key: 'direct', label: 'Connection String', obj: [] },
|
||||
connectFrameworks,
|
||||
{ key: 'orms', label: 'ORMs', obj: ORMS },
|
||||
]
|
||||
const frameworks = !connectFrameworks ? FRAMEWORKS : connectFrameworks.obj
|
||||
|
||||
const [showConnect, setShowConnect] = useQueryState(
|
||||
'showConnect',
|
||||
parseAsBoolean.withDefault(false)
|
||||
)
|
||||
|
||||
const [connectionObject, setConnectionObject] = useState<ConnectionType[]>(FRAMEWORKS)
|
||||
const [connectionObject, setConnectionObject] = useState<ConnectionType[]>(frameworks)
|
||||
const [selectedParent, setSelectedParent] = useState(connectionObject[0].key) // aka nextjs
|
||||
const [selectedChild, setSelectedChild] = useState(
|
||||
connectionObject.find((item) => item.key === selectedParent)?.children[0]?.key ?? ''
|
||||
@@ -54,6 +66,8 @@ export const Connect = () => {
|
||||
?.children.find((child) => child.key === selectedChild)?.children[0]?.key || ''
|
||||
)
|
||||
|
||||
const isFrameworkSelected = frameworks.some((x) => x.key === selectedParent)
|
||||
|
||||
const { data: settings } = useProjectSettingsV2Query({ projectRef }, { enabled: showConnect })
|
||||
const { can: canReadAPIKeys } = useAsyncCheckProjectPermissions(
|
||||
PermissionAction.READ,
|
||||
@@ -109,8 +123,8 @@ export const Connect = () => {
|
||||
|
||||
function handleConnectionType(type: string) {
|
||||
if (type === 'frameworks') {
|
||||
setConnectionObject(FRAMEWORKS)
|
||||
handleConnectionTypeChange(FRAMEWORKS)
|
||||
setConnectionObject(frameworks)
|
||||
handleConnectionTypeChange(frameworks)
|
||||
}
|
||||
|
||||
if (type === 'mobiles') {
|
||||
@@ -207,14 +221,14 @@ export const Connect = () => {
|
||||
|
||||
<Tabs_Shadcn_ defaultValue="direct" onValueChange={(value) => handleConnectionType(value)}>
|
||||
<TabsList_Shadcn_ className={cn('flex overflow-x-scroll gap-x-4', DIALOG_PADDING_X)}>
|
||||
{CONNECTION_TYPES.map((type) => (
|
||||
{connectionTypes.map((type) => (
|
||||
<TabsTrigger_Shadcn_ key={type.key} value={type.key} className="px-0">
|
||||
{type.label}
|
||||
</TabsTrigger_Shadcn_>
|
||||
))}
|
||||
</TabsList_Shadcn_>
|
||||
|
||||
{CONNECTION_TYPES.map((type) => {
|
||||
{connectionTypes.map((type) => {
|
||||
const hasChildOptions =
|
||||
(connectionObject.find((parent) => parent.key === selectedParent)?.children.length ||
|
||||
0) > 0
|
||||
@@ -290,11 +304,18 @@ export const Connect = () => {
|
||||
<p className="text-xs text-foreground-lighter my-3">
|
||||
Add the following files below to your application
|
||||
</p>
|
||||
<ConnectTabContent
|
||||
projectKeys={projectKeys}
|
||||
filePath={filePath}
|
||||
className="rounded-b-none"
|
||||
/>
|
||||
{!!connectFrameworks && isFrameworkSelected ? (
|
||||
<ConnectTabContentCustom
|
||||
projectKeys={projectKeys}
|
||||
framework={frameworks.find((x) => x.key === selectedParent)}
|
||||
/>
|
||||
) : (
|
||||
<ConnectTabContent
|
||||
projectKeys={projectKeys}
|
||||
filePath={filePath}
|
||||
className="rounded-b-none"
|
||||
/>
|
||||
)}
|
||||
<Panel.Notice
|
||||
className="border border-t-0 rounded-lg rounded-t-none"
|
||||
title="New API keys coming 2025"
|
||||
|
||||
@@ -14,16 +14,17 @@ import {
|
||||
Popover_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { ConnectionType } from './Connect.constants'
|
||||
import { ConnectionIcon } from './ConnectionIcon'
|
||||
|
||||
interface ConnectDropdownProps {
|
||||
state: string
|
||||
updateState: (state: string) => void
|
||||
label: string
|
||||
items: any[]
|
||||
items: ConnectionType[]
|
||||
}
|
||||
|
||||
const ConnectDropdown = ({
|
||||
export const ConnectDropdown = ({
|
||||
state,
|
||||
updateState,
|
||||
label,
|
||||
@@ -53,11 +54,7 @@ const ConnectDropdown = ({
|
||||
iconRight={<ChevronDown strokeWidth={1.5} />}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedItem?.icon ? (
|
||||
<ConnectionIcon connection={selectedItem.icon} />
|
||||
) : (
|
||||
<Box size={12} />
|
||||
)}
|
||||
{selectedItem?.icon ? <ConnectionIcon icon={selectedItem.icon} /> : <Box size={12} />}
|
||||
{selectedItem?.label}
|
||||
</div>
|
||||
</Button>
|
||||
@@ -79,7 +76,7 @@ const ConnectDropdown = ({
|
||||
}}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
{item.icon ? <ConnectionIcon connection={item.icon} /> : <Box size={12} />}
|
||||
{item.icon ? <ConnectionIcon icon={item.icon} /> : <Box size={12} />}
|
||||
{item.label}
|
||||
<Check
|
||||
size={15}
|
||||
@@ -94,5 +91,3 @@ const ConnectDropdown = ({
|
||||
</Popover_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConnectDropdown
|
||||
@@ -27,7 +27,7 @@ interface ConnectContentTabProps extends HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabProps>(
|
||||
export const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabProps>(
|
||||
({ projectKeys, filePath, ...props }, ref) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: selectedOrg } = useSelectedOrganizationQuery()
|
||||
@@ -102,5 +102,3 @@ const ConnectTabContent = forwardRef<HTMLDivElement, ConnectContentTabProps>(
|
||||
)
|
||||
|
||||
ConnectTabContent.displayName = 'ConnectTabContent'
|
||||
|
||||
export default ConnectTabContent
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { cn, SimpleCodeBlock } from 'ui'
|
||||
import { ConnectionType } from './Connect.constants'
|
||||
import { projectKeys } from './Connect.types'
|
||||
import {
|
||||
ConnectTabContent,
|
||||
ConnectTabs,
|
||||
ConnectTabTrigger,
|
||||
ConnectTabTriggers,
|
||||
} from './ConnectTabs'
|
||||
|
||||
interface ConnectTabContentCustomProps {
|
||||
projectKeys: projectKeys
|
||||
framework?: ConnectionType
|
||||
}
|
||||
|
||||
export const ConnectTabContentCustom = ({
|
||||
projectKeys,
|
||||
framework,
|
||||
}: ConnectTabContentCustomProps) => {
|
||||
const { files = [] } = framework ?? {}
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (framework?.files) setSelectedTab(framework.files[0].name)
|
||||
}, [framework])
|
||||
|
||||
return (
|
||||
<div className={cn('border rounded-lg rounded-b-none')}>
|
||||
<ConnectTabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<ConnectTabTriggers>
|
||||
{files.map((x) => (
|
||||
<ConnectTabTrigger key={`${x.name}-tab`} value={x.name} />
|
||||
))}
|
||||
</ConnectTabTriggers>
|
||||
|
||||
{files.map((x) => {
|
||||
const format = x.name.split('.')[1] ?? 'bash'
|
||||
const content = x.content
|
||||
.replaceAll('{{apiUrl}}', projectKeys.apiUrl ?? '')
|
||||
.replaceAll('{{anonKey}}', projectKeys.anonKey ?? '')
|
||||
.replaceAll('{{publishableKey}}', projectKeys.publishableKey ?? '')
|
||||
return (
|
||||
<ConnectTabContent key={`${x.name}-content`} value={x.name}>
|
||||
<SimpleCodeBlock className={format} parentClassName="min-h-72">
|
||||
{content}
|
||||
</SimpleCodeBlock>
|
||||
</ConnectTabContent>
|
||||
)
|
||||
})}
|
||||
</ConnectTabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,20 +12,26 @@ interface ConnectTabTriggersProps {
|
||||
|
||||
interface ConnectFileTabProps {
|
||||
children: ReactNode[]
|
||||
value?: string
|
||||
onValueChange?: (value: string) => void
|
||||
}
|
||||
|
||||
interface ConnectTabContentProps {
|
||||
children: ReactNode
|
||||
value: string
|
||||
}
|
||||
const ConnectTabs = ({ children }: ConnectFileTabProps) => {
|
||||
const ConnectTabs = ({ children, value, onValueChange }: ConnectFileTabProps) => {
|
||||
const firstChild = children[0]
|
||||
|
||||
const defaultValue = isValidElement(firstChild)
|
||||
? (firstChild.props as any)?.children[0]?.props?.value || ''
|
||||
: null
|
||||
|
||||
return <Tabs_Shadcn_ defaultValue={defaultValue}>{children}</Tabs_Shadcn_>
|
||||
return (
|
||||
<Tabs_Shadcn_ defaultValue={defaultValue} value={value} onValueChange={onValueChange}>
|
||||
{children}
|
||||
</Tabs_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
const ConnectTabTrigger = ({ value }: ConnectTabTriggerProps) => {
|
||||
|
||||
@@ -4,26 +4,29 @@ import Image from 'next/image'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
|
||||
interface ConnectionIconProps {
|
||||
connection: any
|
||||
icon: string
|
||||
}
|
||||
|
||||
export const ConnectionIcon = ({ connection }: ConnectionIconProps) => {
|
||||
export const ConnectionIcon = ({ icon }: ConnectionIconProps) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
const imageFolder = ['ionic-angular'].includes(connection) ? 'icons/frameworks' : 'libraries'
|
||||
const imageFolder = ['ionic-angular'].includes(icon) ? 'icons/frameworks' : 'libraries'
|
||||
const imageExtension = imageFolder === 'icons/frameworks' ? '' : '-icon'
|
||||
|
||||
return (
|
||||
<Image
|
||||
className="transition-all group-hover:scale-110"
|
||||
src={`${BASE_PATH}/img/${imageFolder}/${connection.toLowerCase()}${
|
||||
['expo', 'nextjs', 'prisma', 'drizzle', 'astro', 'remix'].includes(connection.toLowerCase())
|
||||
const iconImgSrc = icon.startsWith('http')
|
||||
? icon
|
||||
: `${BASE_PATH}/img/${imageFolder}/${icon.toLowerCase()}${
|
||||
['expo', 'nextjs', 'prisma', 'drizzle', 'astro', 'remix'].includes(icon.toLowerCase())
|
||||
? resolvedTheme?.includes('dark')
|
||||
? '-dark'
|
||||
: ''
|
||||
: ''
|
||||
}${imageExtension}.svg`}
|
||||
alt={`${connection} logo`}
|
||||
}${imageExtension}.svg`
|
||||
|
||||
return (
|
||||
<Image
|
||||
className="transition-all group-hover:scale-110"
|
||||
src={iconImgSrc}
|
||||
alt={`${icon} logo`}
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types'
|
||||
|
||||
import {
|
||||
ConnectTabContent,
|
||||
ConnectTabs,
|
||||
ConnectTabTrigger,
|
||||
ConnectTabTriggers,
|
||||
ConnectTabContent,
|
||||
} from 'components/interfaces/Connect/ConnectTabs'
|
||||
import { SimpleCodeBlock } from 'ui'
|
||||
|
||||
@@ -154,4 +154,6 @@ export const createClient = (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
// [Joshen] Used as a dynamic import
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default ContentFile
|
||||
@@ -2,7 +2,7 @@ import Authentication from 'components/interfaces/Docs/Authentication'
|
||||
import Introduction from 'components/interfaces/Docs/Introduction'
|
||||
import RpcIntroduction from 'components/interfaces/Docs/Pages/Rpc/Introduction'
|
||||
import TablesIntroduction from 'components/interfaces/Docs/Pages/Tables/Introduction'
|
||||
import UserManagement from 'components/interfaces/Docs/Pages/UserManagement'
|
||||
import { UserManagement } from 'components/interfaces/Docs/Pages/UserManagement'
|
||||
|
||||
interface GeneralContentProps {
|
||||
page?: string
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { makeRandomString } from 'lib/helpers'
|
||||
import CodeSnippet from '../CodeSnippet'
|
||||
import Snippets from '../Snippets'
|
||||
@@ -14,11 +15,15 @@ interface UserManagementProps {
|
||||
showApiKey: string
|
||||
}
|
||||
|
||||
export default function UserManagement({ selectedLang, showApiKey }: UserManagementProps) {
|
||||
export const UserManagement = ({ selectedLang, showApiKey }: UserManagementProps) => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef } = useParams()
|
||||
const keyToShow = showApiKey ? showApiKey : 'SUPABASE_KEY'
|
||||
|
||||
const { authenticationSignInProviders } = useIsFeatureEnabled([
|
||||
'authentication:sign_in_providers',
|
||||
])
|
||||
|
||||
const { data: settings } = useProjectSettingsV2Query({ projectRef })
|
||||
const protocol = settings?.app_config?.protocol ?? 'https'
|
||||
const hostEndpoint = settings?.app_config?.endpoint ?? ''
|
||||
@@ -157,72 +162,80 @@ export default function UserManagement({ selectedLang, showApiKey }: UserManagem
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<h2 className="doc-heading">Log in with Third Party OAuth</h2>
|
||||
<div className="doc-section ">
|
||||
<article className="code-column text-foreground">
|
||||
<p>
|
||||
Users can log in with Third Party OAuth like Google, Facebook, GitHub, and more. You
|
||||
must first enable each of these in the Auth Providers settings{' '}
|
||||
<span className="text-green-500">
|
||||
<Link key={'AUTH'} href={`/project/${router.query.ref}/auth/providers`}>
|
||||
here
|
||||
</Link>
|
||||
</span>{' '}
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
View all the available{' '}
|
||||
<a
|
||||
href="https://supabase.com/docs/guides/auth#providers"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Third Party OAuth providers
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
After they have logged in, all interactions using the Supabase JS client will be
|
||||
performed as "that user".
|
||||
</p>
|
||||
<p>
|
||||
Generate your Client ID and secret from:{` `}
|
||||
<a
|
||||
href="https://console.developers.google.com/apis/credentials"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Google
|
||||
</a>
|
||||
,{` `}
|
||||
<a href="https://github.com/settings/applications/new" target="_blank" rel="noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
,{` `}
|
||||
<a href="https://gitlab.com/oauth/applications" target="_blank" rel="noreferrer">
|
||||
GitLab
|
||||
</a>
|
||||
,{` `}
|
||||
<a href="https://developers.facebook.com/apps/" target="_blank" rel="noreferrer">
|
||||
Facebook
|
||||
</a>
|
||||
,{` `}
|
||||
<a
|
||||
href="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Bitbucket
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</article>
|
||||
<article className="code">
|
||||
<CodeSnippet
|
||||
selectedLang={selectedLang}
|
||||
snippet={Snippets.authThirdPartyLogin(endpoint, keyToShow)}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
{authenticationSignInProviders && (
|
||||
<>
|
||||
<h2 className="doc-heading">Log in with Third Party OAuth</h2>
|
||||
<div className="doc-section ">
|
||||
<article className="code-column text-foreground">
|
||||
<p>
|
||||
Users can log in with Third Party OAuth like Google, Facebook, GitHub, and more. You
|
||||
must first enable each of these in the Auth Providers settings{' '}
|
||||
<span className="text-green-500">
|
||||
<Link key={'AUTH'} href={`/project/${router.query.ref}/auth/providers`}>
|
||||
here
|
||||
</Link>
|
||||
</span>{' '}
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
View all the available{' '}
|
||||
<a
|
||||
href="https://supabase.com/docs/guides/auth#providers"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Third Party OAuth providers
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
After they have logged in, all interactions using the Supabase JS client will be
|
||||
performed as "that user".
|
||||
</p>
|
||||
<p>
|
||||
Generate your Client ID and secret from:{` `}
|
||||
<a
|
||||
href="https://console.developers.google.com/apis/credentials"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Google
|
||||
</a>
|
||||
,{` `}
|
||||
<a
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
,{` `}
|
||||
<a href="https://gitlab.com/oauth/applications" target="_blank" rel="noreferrer">
|
||||
GitLab
|
||||
</a>
|
||||
,{` `}
|
||||
<a href="https://developers.facebook.com/apps/" target="_blank" rel="noreferrer">
|
||||
Facebook
|
||||
</a>
|
||||
,{` `}
|
||||
<a
|
||||
href="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Bitbucket
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</article>
|
||||
<article className="code">
|
||||
<CodeSnippet
|
||||
selectedLang={selectedLang}
|
||||
snippet={Snippets.authThirdPartyLogin(endpoint, keyToShow)}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="doc-heading">User</h2>
|
||||
<div className="doc-section ">
|
||||
|
||||
@@ -6,20 +6,39 @@ import { useParams } from 'common'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { cn } from 'ui'
|
||||
|
||||
interface ExampleProjectProps {
|
||||
framework: string
|
||||
title: string
|
||||
description: string
|
||||
url: string
|
||||
framework?: string
|
||||
iconUrl?: string
|
||||
}
|
||||
|
||||
const ExampleProject = ({ framework, title, description, url }: ExampleProjectProps) => {
|
||||
export const ExampleProject = ({
|
||||
framework,
|
||||
title,
|
||||
description,
|
||||
url,
|
||||
iconUrl,
|
||||
}: ExampleProjectProps) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const iconImgSrc = iconUrl
|
||||
? iconUrl
|
||||
: !!framework
|
||||
? `${BASE_PATH}/img/libraries/${framework.toLowerCase()}${
|
||||
['expo', 'nextjs'].includes(framework.toLowerCase())
|
||||
? resolvedTheme?.includes('dark')
|
||||
? '-dark'
|
||||
: ''
|
||||
: ''
|
||||
}-icon.svg`
|
||||
: ''
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -35,23 +54,17 @@ const ExampleProject = ({ framework, title, description, url }: ExampleProjectPr
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
className={cn(
|
||||
'group relative',
|
||||
'border bg-surface-100 border-overlay',
|
||||
'flex h-32 flex-row rounded-md p-4 hover:bg-overlay-hover',
|
||||
'transition duration-150 ease-in-out',
|
||||
].join(' ')}
|
||||
'transition duration-150 ease-in-out'
|
||||
)}
|
||||
>
|
||||
<div className="mr-4 flex flex-col">
|
||||
<img
|
||||
className="transition-all group-hover:scale-110"
|
||||
src={`${BASE_PATH}/img/libraries/${framework.toLowerCase()}${
|
||||
['expo', 'nextjs'].includes(framework.toLowerCase())
|
||||
? resolvedTheme?.includes('dark')
|
||||
? '-dark'
|
||||
: ''
|
||||
: ''
|
||||
}-icon.svg`}
|
||||
src={iconImgSrc}
|
||||
alt={`${framework} logo`}
|
||||
width={26}
|
||||
height={26}
|
||||
@@ -62,16 +75,11 @@ const ExampleProject = ({ framework, title, description, url }: ExampleProjectPr
|
||||
<p className="text-sm text-foreground-light">{description}</p>
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
absolute
|
||||
right-4
|
||||
top-3
|
||||
text-foreground-lighter
|
||||
transition-all
|
||||
duration-200
|
||||
group-hover:right-3
|
||||
group-hover:text-foreground
|
||||
"
|
||||
className={cn(
|
||||
'absolute right-4 top-3',
|
||||
'text-foreground-lighter transition-all duration-200',
|
||||
'group-hover:right-3 group-hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<ChevronRight />
|
||||
</div>
|
||||
@@ -79,5 +87,3 @@ const ExampleProject = ({ framework, title, description, url }: ExampleProjectPr
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExampleProject
|
||||
@@ -42,7 +42,10 @@ export const APIKeys = () => {
|
||||
])
|
||||
|
||||
const availableLanguages = [
|
||||
...(javascriptExampleEnabled ? [{ name: 'Javascript', key: 'js' }] : []),
|
||||
{
|
||||
name: javascriptExampleEnabled ? 'Javascript' : 'Typescript',
|
||||
key: 'js',
|
||||
},
|
||||
...(dartExampleEnabled ? [{ name: 'Dart', key: 'dart' }] : []),
|
||||
]
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(availableLanguages[0])
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { IntegrationProjectConnection } from 'data/integrations/integration
|
||||
import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref'
|
||||
import type { ProjectInfo } from 'data/projects/projects-query'
|
||||
import type { ResourceWarning } from 'data/usage/resource-warnings-query'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import InlineSVG from 'react-inlinesvg'
|
||||
import { inferProjectStatus } from './ProjectCard.utils'
|
||||
@@ -29,6 +30,10 @@ const ProjectCard = ({
|
||||
const { name, ref: projectRef } = project
|
||||
const desc = `${project.cloud_provider} | ${project.region}`
|
||||
|
||||
const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([
|
||||
'project_homepage:show_instance_size',
|
||||
])
|
||||
|
||||
const isBranchingEnabled = project.preview_branch_refs?.length > 0
|
||||
const isGithubIntegrated = githubIntegration !== undefined
|
||||
const isVercelIntegrated = vercelIntegration !== undefined
|
||||
@@ -45,7 +50,9 @@ const ProjectCard = ({
|
||||
<p className="flex-shrink truncate text-sm pr-4">{name}</p>
|
||||
<span className="text-sm lowercase text-foreground-light">{desc}</span>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{project.status !== 'INACTIVE' && <ComputeBadgeWrapper project={project} />}
|
||||
{project.status !== 'INACTIVE' && projectHomepageShowInstanceSize && (
|
||||
<ComputeBadgeWrapper project={project} />
|
||||
)}
|
||||
{isVercelIntegrated && (
|
||||
<div className="w-fit p-1 border rounded-md flex items-center text-black dark:text-white">
|
||||
<InlineSVG
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { default as ClientLibrary } from './ClientLibrary'
|
||||
export { default as ExampleProject } from './ExampleProject'
|
||||
@@ -18,7 +18,9 @@ const CATEGORIES = [
|
||||
] as const
|
||||
|
||||
export const AvailableIntegrations = () => {
|
||||
const showStripeWrapper = useIsFeatureEnabled('integrations:show_stripe_wrapper')
|
||||
const { integrationsShowStripeWrapper } = useIsFeatureEnabled([
|
||||
'integrations:show_stripe_wrapper',
|
||||
])
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useQueryState(
|
||||
'category',
|
||||
@@ -41,7 +43,7 @@ export const AvailableIntegrations = () => {
|
||||
const installedIds = installedIntegrations.map((i) => i.id)
|
||||
|
||||
// available integrations for install
|
||||
const availableIntegrations = showStripeWrapper
|
||||
const availableIntegrations = integrationsShowStripeWrapper
|
||||
? allIntegrations
|
||||
: allIntegrations.filter((x) => x.id !== 'stripe_wrapper')
|
||||
const integrationsByCategory =
|
||||
|
||||
@@ -8,7 +8,16 @@ import { useParams } from 'common'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import type { FDW } from 'data/fdw/fdws-query'
|
||||
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { Badge, Sheet, SheetContent, TableCell, TableRow } from 'ui'
|
||||
import {
|
||||
Badge,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { INTEGRATIONS } from '../Landing/Integrations.constants'
|
||||
import DeleteWrapperModal from './DeleteWrapperModal'
|
||||
import { EditWrapperSheet } from './EditWrapperSheet'
|
||||
@@ -74,7 +83,12 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => {
|
||||
<div className="relative w-3 h-3 flex items-center justify-center">
|
||||
{integration.icon({ className: 'p-0' })}
|
||||
</div>
|
||||
{target}{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="truncate max-w-28">{target}</TooltipTrigger>
|
||||
<TooltipContent className="max-w-64 whitespace-pre-wrap break-words">
|
||||
{target}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<ChevronRight
|
||||
size={12}
|
||||
strokeWidth={1.5}
|
||||
@@ -85,7 +99,14 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => {
|
||||
<Link href={`/project/${ref}/editor/${table.id}`}>
|
||||
<Badge className="transition hover:bg-surface-300 pl-5 rounded-l-none gap-2 h-6 font-mono text-[0.75rem] border-l-0">
|
||||
<Table2 size={12} strokeWidth={1.5} className="text-foreground-lighter/50" />
|
||||
{table.schema}.{table.table_name}
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="truncate max-w-28">
|
||||
{table.schema}.{table.table_name}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-64 whitespace-pre-wrap break-words">
|
||||
{table.schema}.{table.table_name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldSection,
|
||||
ScaffoldSectionContent,
|
||||
ScaffoldSectionDetail,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import { CustomContentTypes } from 'hooks/custom-content/CustomContent.types'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
|
||||
interface CustomDocumentProps {
|
||||
doc: CustomContentTypes['organizationLegalDocuments'][number]
|
||||
}
|
||||
|
||||
export const CustomDocument = ({ doc }: CustomDocumentProps) => {
|
||||
return (
|
||||
<ScaffoldContainer id={doc.id}>
|
||||
<ScaffoldSection>
|
||||
<ScaffoldSectionDetail className="sticky top-12 flex flex-col gap-y-8">
|
||||
<p className="text-base m-0">{doc.name}</p>
|
||||
<p className="text-sm text-foreground-light m-0">{doc.description}</p>
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent className="flex items-center justify-center h-full">
|
||||
<Button asChild type="default" iconRight={<ExternalLink />}>
|
||||
<a download href={doc.action.url} target="_blank" rel="noreferrer noopener">
|
||||
{doc.action.text}
|
||||
</a>
|
||||
</Button>
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { ScaffoldContainer, ScaffoldDivider, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
|
||||
import { Fragment } from 'react'
|
||||
import { CustomDocument } from './CustomDocument'
|
||||
import { DPA } from './DPA'
|
||||
import { HIPAA } from './HIPAA'
|
||||
import { SecurityQuestionnaire } from './SecurityQuestionnaire'
|
||||
@@ -8,6 +11,19 @@ import { SOC2 } from './SOC2'
|
||||
import { TIA } from './TIA'
|
||||
|
||||
const Documents = () => {
|
||||
const { organizationLegalDocuments } = useCustomContent(['organization:legal_documents'])
|
||||
|
||||
if (Array.isArray(organizationLegalDocuments)) {
|
||||
return organizationLegalDocuments.map((doc, idx) => {
|
||||
return (
|
||||
<Fragment key={doc.id}>
|
||||
<CustomDocument doc={doc} />
|
||||
{idx !== organizationLegalDocuments.length - 1 && <ScaffoldDivider />}
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer id="dpa">
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ const IntegrationSettings = () => {
|
||||
|
||||
const { mutate: deleteGitHubConnection } = useGitHubConnectionDeleteMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('Successfully deleted Github connection')
|
||||
toast.success('Successfully deleted GitHub connection')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { RevokeAppModal } from './RevokeAppModal'
|
||||
|
||||
// [Joshen] Note on nav UX
|
||||
// Kang Ming mentioned that it might be better to split Published Apps and Authorized Apps into 2 separate tabs
|
||||
// to prevent any confusion (case study: Github). Authorized apps could be in the "integrations" tab, but let's
|
||||
// to prevent any confusion (case study: GitHub). Authorized apps could be in the "integrations" tab, but let's
|
||||
// check in again after we wrap up Vercel integration
|
||||
|
||||
const OAuthApps = () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DataPoint } from 'data/analytics/constants'
|
||||
import { useOrgDailyComputeStatsQuery } from 'data/analytics/org-daily-compute-stats-query'
|
||||
import { ComputeUsageMetric, computeUsageMetricLabel } from 'data/analytics/org-daily-stats-query'
|
||||
import type { OrgSubscription } from 'data/subscriptions/types'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import SectionContent from './SectionContent'
|
||||
import { Attribute, AttributeColor } from './Usage.constants'
|
||||
import UsageBarChart from './UsageBarChart'
|
||||
@@ -34,6 +35,8 @@ const Compute = ({ orgSlug, projectRef, startDate, endDate }: ComputeProps) => {
|
||||
endDate,
|
||||
})
|
||||
|
||||
const { billingAll } = useIsFeatureEnabled(['billing:all'])
|
||||
|
||||
const chartData: DataPoint[] = egressData?.data ?? []
|
||||
|
||||
const COMPUTE_TO_COLOR: Record<ComputeUsageMetric, AttributeColor> = {
|
||||
@@ -79,16 +82,18 @@ const Compute = ({ orgSlug, projectRef, startDate, endDate }: ComputeProps) => {
|
||||
name: 'Compute Hours',
|
||||
description:
|
||||
'Amount of hours your projects were active. Each project is a dedicated server and database.\nPaid plans come with $10 in Compute Credits to cover one project running on Micro Compute or parts of any compute add-on.\nBilling is based on the sum of Compute Hours used. Paused projects do not count towards usage.',
|
||||
links: [
|
||||
{
|
||||
name: 'Compute Add-ons',
|
||||
url: 'https://supabase.com/docs/guides/platform/compute-add-ons',
|
||||
},
|
||||
{
|
||||
name: 'Usage-billing for Compute',
|
||||
url: 'https://supabase.com/docs/guides/platform/manage-your-usage/compute',
|
||||
},
|
||||
],
|
||||
links: billingAll
|
||||
? [
|
||||
{
|
||||
name: 'Compute Add-ons',
|
||||
url: 'https://supabase.com/docs/guides/platform/compute-add-ons',
|
||||
},
|
||||
{
|
||||
name: 'Usage-billing for Compute',
|
||||
url: 'https://supabase.com/docs/guides/platform/manage-your-usage/compute',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}}
|
||||
>
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from 'data/analytics/org-daily-stats-query'
|
||||
import type { OrgSubscription } from 'data/subscriptions/types'
|
||||
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { cn } from 'ui'
|
||||
import { BILLING_BREAKDOWN_METRICS } from '../BillingSettings/BillingBreakdown/BillingBreakdown.constants'
|
||||
import BillingMetric from '../BillingSettings/BillingBreakdown/BillingMetric'
|
||||
@@ -32,7 +33,7 @@ const METRICS_TO_HIDE_WITH_NO_USAGE: PricingMetric[] = [
|
||||
PricingMetric.DISK_THROUGHPUT_GP3,
|
||||
]
|
||||
|
||||
const TotalUsage = ({
|
||||
export const TotalUsage = ({
|
||||
orgSlug,
|
||||
projectRef,
|
||||
subscription,
|
||||
@@ -41,6 +42,7 @@ const TotalUsage = ({
|
||||
currentBillingCycleSelected,
|
||||
}: ComputeProps) => {
|
||||
const isUsageBillingEnabled = subscription?.usage_billing_enabled
|
||||
const { billingAll } = useIsFeatureEnabled(['billing:all'])
|
||||
|
||||
const {
|
||||
data: usage,
|
||||
@@ -117,16 +119,18 @@ const TotalUsage = ({
|
||||
description: isUsageBillingEnabled
|
||||
? `Your plan includes a limited amount of usage. If exceeded, you will be charged for the overages. It may take up to 1 hour to refresh.`
|
||||
: `Your plan includes a limited amount of usage. If exceeded, you may experience restrictions, as you are currently not billed for overages. It may take up to 1 hour to refresh.`,
|
||||
links: [
|
||||
{
|
||||
name: 'How billing works',
|
||||
url: 'https://supabase.com/docs/guides/platform/billing-on-supabase',
|
||||
},
|
||||
{
|
||||
name: 'Supabase Plans',
|
||||
url: 'https://supabase.com/pricing',
|
||||
},
|
||||
],
|
||||
links: billingAll
|
||||
? [
|
||||
{
|
||||
name: 'How billing works',
|
||||
url: 'https://supabase.com/docs/guides/platform/billing-on-supabase',
|
||||
},
|
||||
{
|
||||
name: 'Supabase Plans',
|
||||
url: 'https://supabase.com/pricing',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}}
|
||||
>
|
||||
{isLoadingUsage && (
|
||||
@@ -234,5 +238,3 @@ const TotalUsage = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TotalUsage
|
||||
@@ -22,10 +22,10 @@ import { cn, Listbox } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { Restriction } from '../BillingSettings/Restriction'
|
||||
import Activity from './Activity'
|
||||
import Egress from './Egress'
|
||||
import Compute from './Compute'
|
||||
import Egress from './Egress'
|
||||
import SizeAndCounts from './SizeAndCounts'
|
||||
import TotalUsage from './TotalUsage'
|
||||
import { TotalUsage } from './TotalUsage'
|
||||
|
||||
const Usage = () => {
|
||||
const { slug, projectRef } = useParams()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import ContentSnippet from '../ContentSnippet'
|
||||
import { DOCS_CONTENT } from '../ProjectAPIDocs.constants'
|
||||
import type { ContentProps } from './Content.types'
|
||||
|
||||
const UserManagement = ({ language, apikey, endpoint }: ContentProps) => {
|
||||
const { authenticationSignInProviders } = useIsFeatureEnabled([
|
||||
'authentication:sign_in_providers',
|
||||
])
|
||||
return (
|
||||
<>
|
||||
<ContentSnippet selectedLanguage={language} snippet={DOCS_CONTENT.userManagement} />
|
||||
@@ -42,12 +46,14 @@ const UserManagement = ({ language, apikey, endpoint }: ContentProps) => {
|
||||
endpoint={endpoint}
|
||||
snippet={DOCS_CONTENT.smsVerify}
|
||||
/>
|
||||
<ContentSnippet
|
||||
selectedLanguage={language}
|
||||
apikey={apikey}
|
||||
endpoint={endpoint}
|
||||
snippet={DOCS_CONTENT.oauthLogin}
|
||||
/>
|
||||
{authenticationSignInProviders && (
|
||||
<ContentSnippet
|
||||
selectedLanguage={language}
|
||||
apikey={apikey}
|
||||
endpoint={endpoint}
|
||||
snippet={DOCS_CONTENT.oauthLogin}
|
||||
/>
|
||||
)}
|
||||
<ContentSnippet
|
||||
selectedLanguage={language}
|
||||
apikey={apikey}
|
||||
|
||||
+17
-4
@@ -15,6 +15,7 @@ import {
|
||||
} from 'data/read-replicas/replicas-status-query'
|
||||
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
|
||||
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import {
|
||||
@@ -44,7 +45,7 @@ interface NodeData {
|
||||
id: string
|
||||
provider: string
|
||||
region: Region
|
||||
computeSize: string
|
||||
computeSize?: string
|
||||
status: string
|
||||
inserted_at: string
|
||||
}
|
||||
@@ -109,6 +110,10 @@ export const PrimaryNode = ({ data }: NodeProps<PrimaryNodeData>) => {
|
||||
// [Joshen] Just FYI Handles cannot be conditionally rendered
|
||||
const { provider, region, computeSize, numReplicas, numRegions, hasLoadBalancer } = data
|
||||
|
||||
const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([
|
||||
'project_homepage:show_instance_size',
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
@@ -133,8 +138,12 @@ export const PrimaryNode = ({ data }: NodeProps<PrimaryNodeData>) => {
|
||||
</p>
|
||||
<p className="flex items-center gap-x-1">
|
||||
<span className="text-sm text-foreground-light">{provider}</span>
|
||||
<span className="text-sm text-foreground-light">•</span>
|
||||
<span className="text-sm text-foreground-light">{computeSize}</span>
|
||||
{projectHomepageShowInstanceSize && (
|
||||
<>
|
||||
<span className="text-sm text-foreground-light">•</span>
|
||||
<span className="text-sm text-foreground-light">{computeSize}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,6 +195,10 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
|
||||
PermissionAction.CREATE,
|
||||
'projects'
|
||||
)
|
||||
const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([
|
||||
'project_homepage:show_instance_size',
|
||||
])
|
||||
|
||||
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
|
||||
|
||||
const { data: databaseStatuses } = useReadReplicasStatusesQuery({ projectRef: ref })
|
||||
@@ -289,7 +302,7 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
|
||||
<p className="text-sm text-foreground-light">{region.name}</p>
|
||||
<p className="flex text-sm text-foreground-light items-center gap-x-1">
|
||||
<span>{provider}</span>
|
||||
{!!computeSize && (
|
||||
{projectHomepageShowInstanceSize && !!computeSize && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{computeSize}</span>
|
||||
|
||||
+7
-1
@@ -20,6 +20,7 @@ import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
|
||||
import { Database, useReadReplicasQuery } from 'data/read-replicas/replicas-query'
|
||||
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
|
||||
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import type { AWS_REGIONS_KEYS } from 'shared-data'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
@@ -51,6 +52,9 @@ const MapView = ({
|
||||
}: MapViewProps) => {
|
||||
const { ref } = useParams()
|
||||
const dbSelectorState = useDatabaseSelectorStateSnapshot()
|
||||
const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([
|
||||
'project_homepage:show_instance_size',
|
||||
])
|
||||
|
||||
const [mount, setMount] = useState(false)
|
||||
const [zoom, setZoom] = useState<number>(1.5)
|
||||
@@ -286,7 +290,9 @@ const MapView = ({
|
||||
<Badge variant="warning">Unhealthy</Badge>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-foreground-light">AWS • {database.size}</p>
|
||||
<p className="text-xs text-foreground-light">
|
||||
AWS{projectHomepageShowInstanceSize ? ` • ${database.size}` : ''}
|
||||
</p>
|
||||
{database.identifier !== ref && (
|
||||
<p className="text-xs text-foreground-light">Created on: {created}</p>
|
||||
)}
|
||||
|
||||
@@ -58,6 +58,7 @@ const LogsQueryPanel = ({
|
||||
onDateChange,
|
||||
}: LogsQueryPanelProps) => {
|
||||
const [showReference, setShowReference] = useState(false)
|
||||
const { logsTemplates } = useIsFeatureEnabled(['logs:templates'])
|
||||
|
||||
const {
|
||||
projectAuthAll: authEnabled,
|
||||
@@ -125,7 +126,7 @@ const LogsQueryPanel = ({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{IS_PLATFORM && (
|
||||
{IS_PLATFORM && logsTemplates && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<ChevronDown />}>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { auth } from 'lib/gotrue'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const SignInPartner = () => {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
const params = new URLSearchParams(window.location.hash.substring(1))
|
||||
|
||||
const partner = params.get('partner')
|
||||
const token = params.get('id_token')
|
||||
|
||||
const { data } = await auth.getSession()
|
||||
|
||||
if (!data.session && partner && token) {
|
||||
try {
|
||||
await auth.signInWithIdToken({ provider: partner, token })
|
||||
} finally {
|
||||
router.replace({ pathname: '/sign-in-mfa' })
|
||||
}
|
||||
} else {
|
||||
router.replace({ pathname: '/sign-in' })
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto w-full flex flex-col items-center justify-center gap-y-6">
|
||||
<Loader2 className="animate-spin" />
|
||||
<h2 className="text-lg text-center">Signing in to Supabase Dashboard</h2>
|
||||
<p className="text-xs text-foreground-lighter text-center max-w-[220px] sm:max-w-full">
|
||||
By continuing, you agree to Supabase's{' '}
|
||||
<InlineLink
|
||||
href="https://supabase.com/terms"
|
||||
className="text-foreground-lighter hover:text-foreground"
|
||||
>
|
||||
Terms of Service
|
||||
</InlineLink>{' '}
|
||||
and{' '}
|
||||
<InlineLink
|
||||
href="https://supabase.com/privacy"
|
||||
className="text-foreground-lighter hover:text-foreground"
|
||||
>
|
||||
Privacy Policy
|
||||
</InlineLink>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export const LibrarySuggestions = ({ library }: LibrarySuggestionsProps) => {
|
||||
<div className="px-6 flex flex-col gap-y-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Found an issue or a bug? Try searching our Github issues or submit a new one.
|
||||
Found an issue or a bug? Try searching our GitHub issues or submit a new one.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 overflow-x-auto">
|
||||
@@ -37,7 +37,7 @@ export const LibrarySuggestions = ({ library }: LibrarySuggestionsProps) => {
|
||||
<div>
|
||||
<Button asChild type="default" icon={<ExternalLink size={14} strokeWidth={1.5} />}>
|
||||
<Link href={lib.url} target="_blank" rel="noreferrer">
|
||||
View Github issues
|
||||
View GitHub issues
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ export const LibrarySuggestions = ({ library }: LibrarySuggestionsProps) => {
|
||||
<div>
|
||||
<Button asChild type="default" icon={<ExternalLink size={14} strokeWidth={1.5} />}>
|
||||
<Link href="https://github.com/supabase/supabase" target="_blank" rel="noreferrer">
|
||||
View Github issues
|
||||
View GitHub issues
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@ const Success = ({
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Search on Github discussions
|
||||
Search on GitHub discussions
|
||||
</Link>
|
||||
</Button>,
|
||||
]}
|
||||
|
||||
@@ -584,7 +584,7 @@ export const SupportFormV2 = ({
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-x-1 underline hover:text-foreground transition"
|
||||
>
|
||||
Github discussions
|
||||
GitHub discussions
|
||||
<ExternalLink size={14} strokeWidth={2} />
|
||||
</Link>
|
||||
<span> for a quick answer</span>
|
||||
|
||||
+6
-1
@@ -187,7 +187,12 @@ const ColumnType = ({
|
||||
<PopoverContent_Shadcn_ className="w-[460px] p-0" side="bottom" align="center">
|
||||
<ScrollArea className="h-[335px]">
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search types..." />
|
||||
<CommandInput_Shadcn_
|
||||
placeholder="Search types..."
|
||||
// [Joshen] Addresses style issues when this component is being used in the old Form component
|
||||
// Specifically in WrapperDynamicColumns - can be cleaned up once we're no longer using that
|
||||
className="!bg-transparent focus:!shadow-none focus:!ring-0"
|
||||
/>
|
||||
<CommandEmpty_Shadcn_>Type not found.</CommandEmpty_Shadcn_>
|
||||
|
||||
<CommandList_Shadcn_>
|
||||
|
||||
@@ -230,7 +230,7 @@ export const BranchDropdown = () => {
|
||||
<MessageCircle size={14} strokeWidth={1} className="text-muted mt-0.5" />
|
||||
<div>
|
||||
<p>Branching feedback</p>
|
||||
<p className="text-lighter">Join Github Discussion</p>
|
||||
<p className="text-lighter">Join GitHub Discussion</p>
|
||||
</div>
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
import { Lock } from 'lucide-react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
|
||||
import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import type { CommandOptions } from 'ui-patterns/CommandMenu'
|
||||
import { useRegisterCommands } from 'ui-patterns/CommandMenu'
|
||||
import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering'
|
||||
import { useParams } from 'common'
|
||||
import { IRouteCommand } from 'ui-patterns/CommandMenu/internal/types'
|
||||
|
||||
export function useAuthGotoCommands(options?: CommandOptions) {
|
||||
let { ref } = useParams()
|
||||
ref ||= '_'
|
||||
|
||||
const {
|
||||
authenticationSignInProviders,
|
||||
authenticationThirdPartyAuth,
|
||||
authenticationRateLimits,
|
||||
authenticationEmails,
|
||||
authenticationMultiFactor,
|
||||
authenticationAttackProtection,
|
||||
authenticationAdvanced,
|
||||
} = useIsFeatureEnabled([
|
||||
'authentication:sign_in_providers',
|
||||
'authentication:third_party_auth',
|
||||
'authentication:rate_limits',
|
||||
'authentication:emails',
|
||||
'authentication:multi_factor',
|
||||
'authentication:attack_protection',
|
||||
'authentication:advanced',
|
||||
])
|
||||
|
||||
useRegisterCommands(
|
||||
'Actions',
|
||||
[
|
||||
@@ -47,20 +67,28 @@ export function useAuthGotoCommands(options?: CommandOptions) {
|
||||
route: `/project/${ref}/auth/policies`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-auth-providers',
|
||||
name: 'Providers',
|
||||
value: 'Auth: Providers (Social Login, SSO)',
|
||||
route: `/project/${ref}/auth/providers`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-auth-providers',
|
||||
name: 'Providers (Third Party)',
|
||||
value: 'Auth: Providers (Third Party)',
|
||||
route: `/project/${ref}/auth/third-party`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
...(authenticationSignInProviders
|
||||
? [
|
||||
{
|
||||
id: 'nav-auth-providers',
|
||||
name: 'Providers',
|
||||
value: 'Auth: Providers (Social Login, SSO)',
|
||||
route: `/project/${ref}/auth/providers`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
...(authenticationThirdPartyAuth
|
||||
? [
|
||||
{
|
||||
id: 'nav-auth-providers',
|
||||
name: 'Providers (Third Party)',
|
||||
value: 'Auth: Providers (Third Party)',
|
||||
route: `/project/${ref}/auth/third-party`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'nav-auth-sessions',
|
||||
name: 'Sessions',
|
||||
@@ -68,27 +96,39 @@ export function useAuthGotoCommands(options?: CommandOptions) {
|
||||
route: `/project/${ref}/auth/sessions`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-auth-rate-limits',
|
||||
name: 'Rate Limits',
|
||||
value: 'Auth: Rate Limits',
|
||||
route: `/project/${ref}/auth/rate-limits`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-auth-templates',
|
||||
name: 'Email Templates',
|
||||
value: 'Auth: Email Templates',
|
||||
route: `/project/${ref}/auth/templates`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-auth-mfa',
|
||||
name: 'Multi Factor Authentication (MFA)',
|
||||
value: 'Auth: Multi Factor Authenticaiton (MFA)',
|
||||
route: `/project/${ref}/auth/mfa`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
...(authenticationRateLimits
|
||||
? [
|
||||
{
|
||||
id: 'nav-auth-rate-limits',
|
||||
name: 'Rate Limits',
|
||||
value: 'Auth: Rate Limits',
|
||||
route: `/project/${ref}/auth/rate-limits`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
...(authenticationEmails
|
||||
? [
|
||||
{
|
||||
id: 'nav-auth-templates',
|
||||
name: 'Email Templates',
|
||||
value: 'Auth: Email Templates',
|
||||
route: `/project/${ref}/auth/templates`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
...(authenticationMultiFactor
|
||||
? [
|
||||
{
|
||||
id: 'nav-auth-mfa',
|
||||
name: 'Multi Factor Authentication (MFA)',
|
||||
value: 'Auth: Multi Factor Authenticaiton (MFA)',
|
||||
route: `/project/${ref}/auth/mfa`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'nav-auth-url-configuration',
|
||||
name: 'URL Configuration',
|
||||
@@ -96,13 +136,17 @@ export function useAuthGotoCommands(options?: CommandOptions) {
|
||||
route: `/project/${ref}/auth/url-configuration`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-auth-attack-protection',
|
||||
name: 'Attack Protection',
|
||||
value: 'Auth: Attack Protection',
|
||||
route: `/project/${ref}/auth/protection`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
...(authenticationAttackProtection
|
||||
? [
|
||||
{
|
||||
id: 'nav-auth-attack-protection',
|
||||
name: 'Attack Protection',
|
||||
value: 'Auth: Attack Protection',
|
||||
route: `/project/${ref}/auth/protection`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'nav-auth-auth-hooks',
|
||||
name: 'Auth Hooks',
|
||||
@@ -110,13 +154,17 @@ export function useAuthGotoCommands(options?: CommandOptions) {
|
||||
route: `/project/${ref}/auth/hooks`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-auth-advanced-settings',
|
||||
name: 'Auth Advanced Settings',
|
||||
value: 'Auth: Advanced Settings',
|
||||
route: `/project/${ref}/auth/advanced`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
...(authenticationAdvanced
|
||||
? [
|
||||
{
|
||||
id: 'nav-auth-advanced-settings',
|
||||
name: 'Auth Advanced Settings',
|
||||
value: 'Auth: Advanced Settings',
|
||||
route: `/project/${ref}/auth/advanced`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
{ ...options, deps: [ref] }
|
||||
)
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { Blocks, Code, Database, History, Search } from 'lucide-react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
|
||||
import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import type { CommandOptions } from 'ui-patterns/CommandMenu'
|
||||
import { useRegisterCommands } from 'ui-patterns/CommandMenu'
|
||||
import { useParams } from 'common'
|
||||
import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering'
|
||||
import { IRouteCommand } from 'ui-patterns/CommandMenu/internal/types'
|
||||
|
||||
export function useDatabaseGotoCommands(options?: CommandOptions) {
|
||||
let { ref } = useParams()
|
||||
ref ||= '_'
|
||||
|
||||
const { databaseReplication, databaseRoles } = useIsFeatureEnabled([
|
||||
'database:replication',
|
||||
'database:roles',
|
||||
])
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.QUERY,
|
||||
[
|
||||
@@ -59,20 +66,28 @@ export function useDatabaseGotoCommands(options?: CommandOptions) {
|
||||
route: `/project/${ref}/database/extensions`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-database-roles',
|
||||
name: 'Roles',
|
||||
value: 'Database: Roles',
|
||||
route: `/project/${ref}/database/roles`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-database-replication',
|
||||
name: 'Replication',
|
||||
value: 'Database: Replication',
|
||||
route: `/project/${ref}/database/replication`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
...(databaseRoles
|
||||
? [
|
||||
{
|
||||
id: 'nav-database-roles',
|
||||
name: 'Roles',
|
||||
value: 'Database: Roles',
|
||||
route: `/project/${ref}/database/roles`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
...(databaseReplication
|
||||
? [
|
||||
{
|
||||
id: 'nav-database-replication',
|
||||
name: 'Replication',
|
||||
value: 'Database: Replication',
|
||||
route: `/project/${ref}/database/replication`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'nav-database-hooks',
|
||||
name: 'Webhooks',
|
||||
@@ -144,12 +159,16 @@ export function useDatabaseGotoCommands(options?: CommandOptions) {
|
||||
route: `/project/${ref}/database/indexes`,
|
||||
icon: () => <Database />,
|
||||
},
|
||||
{
|
||||
id: 'run-view-database-roles',
|
||||
name: 'View your roles',
|
||||
route: `/project/${ref}/database/roles`,
|
||||
icon: () => <Database />,
|
||||
},
|
||||
...(databaseRoles
|
||||
? [
|
||||
{
|
||||
id: 'run-view-database-roles',
|
||||
name: 'View your roles',
|
||||
route: `/project/${ref}/database/roles`,
|
||||
icon: () => <Database />,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'run-view-database-backups',
|
||||
name: 'View your backups',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IntegrationDefinition,
|
||||
INTEGRATIONS,
|
||||
} from 'components/interfaces/Integrations/Landing/Integrations.constants'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import type { CommandOptions } from 'ui-patterns/CommandMenu'
|
||||
import { useRegisterCommands } from 'ui-patterns/CommandMenu'
|
||||
|
||||
@@ -12,6 +13,14 @@ export function useIntegrationsGotoCommands(options?: CommandOptions) {
|
||||
let { ref } = useParams()
|
||||
ref ||= '_'
|
||||
|
||||
const { integrationsShowStripeWrapper } = useIsFeatureEnabled([
|
||||
'integrations:show_stripe_wrapper',
|
||||
])
|
||||
|
||||
const allIntegrations = integrationsShowStripeWrapper
|
||||
? INTEGRATIONS
|
||||
: INTEGRATIONS.filter((x) => x.id !== 'stripe_wrapper')
|
||||
|
||||
const getName = (integration: IntegrationDefinition) => {
|
||||
switch (integration.id) {
|
||||
case 'cron':
|
||||
@@ -27,7 +36,7 @@ export function useIntegrationsGotoCommands(options?: CommandOptions) {
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.NAVIGATE,
|
||||
INTEGRATIONS.map((x) => {
|
||||
allIntegrations.map((x) => {
|
||||
return {
|
||||
id: `nav-integrations-${x.id}`,
|
||||
name: x.name,
|
||||
@@ -41,7 +50,7 @@ export function useIntegrationsGotoCommands(options?: CommandOptions) {
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.INTEGRATIONS,
|
||||
INTEGRATIONS.map((x) => {
|
||||
allIntegrations.map((x) => {
|
||||
return {
|
||||
id: `manage-${x.id}`,
|
||||
name: getName(x),
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useParams } from 'common'
|
||||
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import type { CommandOptions } from 'ui-patterns/CommandMenu'
|
||||
import { useRegisterCommands } from 'ui-patterns/CommandMenu'
|
||||
import { IRouteCommand } from 'ui-patterns/CommandMenu/internal/types'
|
||||
|
||||
export function useLogsGotoCommands(options?: CommandOptions) {
|
||||
let { ref } = useParams()
|
||||
ref ||= '_'
|
||||
|
||||
const { logsCollections } = useIsFeatureEnabled(['logs:collections'])
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.NAVIGATE,
|
||||
[
|
||||
@@ -16,42 +20,46 @@ export function useLogsGotoCommands(options?: CommandOptions) {
|
||||
route: `/project/${ref}/logs/explorer`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-postgres',
|
||||
name: 'Postgres Logs',
|
||||
route: `/project/${ref}/logs/postgres-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-postgrest',
|
||||
name: 'PostgREST Logs',
|
||||
route: `/project/${ref}/logs/postgrest-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-pooler',
|
||||
name: 'Pooler Logs',
|
||||
route: `/project/${ref}/logs/pooler-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-auth',
|
||||
name: 'Auth Logs',
|
||||
route: `/project/${ref}/logs/auth-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-storage',
|
||||
name: 'Storage Logs',
|
||||
route: `/project/${ref}/logs/storage-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-realtime',
|
||||
name: 'Realtime Logs',
|
||||
route: `/project/${ref}/logs/realtime-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
...(logsCollections
|
||||
? ([
|
||||
{
|
||||
id: 'nav-logs-postgres',
|
||||
name: 'Postgres Logs',
|
||||
route: `/project/${ref}/logs/postgres-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-postgrest',
|
||||
name: 'PostgREST Logs',
|
||||
route: `/project/${ref}/logs/postgrest-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-pooler',
|
||||
name: 'Pooler Logs',
|
||||
route: `/project/${ref}/logs/pooler-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-auth',
|
||||
name: 'Auth Logs',
|
||||
route: `/project/${ref}/logs/auth-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-storage',
|
||||
name: 'Storage Logs',
|
||||
route: `/project/${ref}/logs/storage-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-logs-realtime',
|
||||
name: 'Realtime Logs',
|
||||
route: `/project/${ref}/logs/realtime-logs`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
] as IRouteCommand[])
|
||||
: []),
|
||||
],
|
||||
{ ...options, deps: [ref] }
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useMemo } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import ClientLibrary from 'components/interfaces/Home/ClientLibrary'
|
||||
import ExampleProject from 'components/interfaces/Home/ExampleProject'
|
||||
import { ExampleProject } from 'components/interfaces/Home/ExampleProject'
|
||||
import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
|
||||
import { DisplayApiSettings, DisplayConfigSettings } from 'components/ui/ProjectSettings'
|
||||
import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query'
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { useParams } from 'common'
|
||||
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import type { CommandOptions } from 'ui-patterns/CommandMenu'
|
||||
import { useRegisterCommands } from 'ui-patterns/CommandMenu'
|
||||
import { IRouteCommand } from 'ui-patterns/CommandMenu/internal/types'
|
||||
|
||||
export function useProjectSettingsGotoCommands(options?: CommandOptions) {
|
||||
let { ref } = useParams()
|
||||
ref ||= '_'
|
||||
|
||||
const { projectSettingsLogDrains, projectSettingsCustomDomains, authenticationSignInProviders } =
|
||||
useIsFeatureEnabled([
|
||||
'project_settings:log_drains',
|
||||
'project_settings:custom_domains',
|
||||
'authentication:sign_in_providers',
|
||||
])
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.NAVIGATE,
|
||||
[
|
||||
@@ -25,7 +34,9 @@ export function useProjectSettingsGotoCommands(options?: CommandOptions) {
|
||||
{
|
||||
id: 'nav-project-settings-auth',
|
||||
name: 'Auth Settings',
|
||||
route: `/project/${ref}/auth/providers`,
|
||||
route: authenticationSignInProviders
|
||||
? `/project/${ref}/auth/providers`
|
||||
: `/project/${ref}/auth/policies`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
@@ -40,12 +51,16 @@ export function useProjectSettingsGotoCommands(options?: CommandOptions) {
|
||||
route: `/project/${ref}/storage/settings`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-project-settings-custom-domains',
|
||||
name: 'Custom Domains',
|
||||
route: `/project/${ref}/settings/general#custom-domains`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
...(projectSettingsCustomDomains
|
||||
? [
|
||||
{
|
||||
id: 'nav-project-settings-custom-domains',
|
||||
name: 'Custom Domains',
|
||||
route: `/project/${ref}/settings/general#custom-domains`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'nav-project-settings-restart-project',
|
||||
name: 'Restart project',
|
||||
@@ -106,12 +121,16 @@ export function useProjectSettingsGotoCommands(options?: CommandOptions) {
|
||||
route: `/project/${ref}/database/settings#banned-ips`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-project-settings-log-drains',
|
||||
name: 'Log drains',
|
||||
route: `/project/${ref}/settings/log-drains`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
...(projectSettingsLogDrains
|
||||
? [
|
||||
{
|
||||
id: 'nav-project-settings-log-drains',
|
||||
name: 'Log drains',
|
||||
route: `/project/${ref}/settings/log-drains`,
|
||||
defaultHidden: true,
|
||||
} as IRouteCommand,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
{ ...options, deps: [ref] }
|
||||
)
|
||||
|
||||
@@ -128,7 +128,7 @@ export const generateSettingsMenu = (
|
||||
{
|
||||
name: 'Authentication',
|
||||
key: 'auth',
|
||||
url: `/project/${ref}/auth/policies`,
|
||||
url: `/project/${ref}/auth/providers`,
|
||||
items: [],
|
||||
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useParams } from 'common'
|
||||
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import type { CommandOptions } from 'ui-patterns/CommandMenu'
|
||||
import { useRegisterCommands } from 'ui-patterns/CommandMenu'
|
||||
|
||||
@@ -7,40 +8,44 @@ export function useReportsGotoCommands(options?: CommandOptions) {
|
||||
let { ref } = useParams()
|
||||
ref ||= '_'
|
||||
|
||||
const { reportsAll } = useIsFeatureEnabled(['reports:all'])
|
||||
|
||||
useRegisterCommands(
|
||||
COMMAND_MENU_SECTIONS.NAVIGATE,
|
||||
[
|
||||
{
|
||||
id: 'nav-reports',
|
||||
name: 'Reports',
|
||||
route: `/project/${ref}/reports`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-api',
|
||||
name: 'API Reports',
|
||||
route: `/project/${ref}/reports/api-overview`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-storage',
|
||||
name: 'Storage Reports',
|
||||
route: `/project/${ref}/reports/storage`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-database',
|
||||
name: 'Database Reports',
|
||||
route: `/project/${ref}/reports/database`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-query-performance',
|
||||
name: 'Query Performance Reports',
|
||||
route: `/project/${ref}/reports/query-performance`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
],
|
||||
reportsAll
|
||||
? [
|
||||
{
|
||||
id: 'nav-reports',
|
||||
name: 'Reports',
|
||||
route: `/project/${ref}/reports`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-api',
|
||||
name: 'API Reports',
|
||||
route: `/project/${ref}/reports/api-overview`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-storage',
|
||||
name: 'Storage Reports',
|
||||
route: `/project/${ref}/reports/storage`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-database',
|
||||
name: 'Database Reports',
|
||||
route: `/project/${ref}/reports/database`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'nav-reports-query-performance',
|
||||
name: 'Query Performance Reports',
|
||||
route: `/project/${ref}/reports/query-performance`,
|
||||
defaultHidden: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
{ ...options, deps: [ref] }
|
||||
)
|
||||
}
|
||||
@@ -3,12 +3,14 @@ import { useTheme } from 'next-themes'
|
||||
import Image from 'next/legacy/image'
|
||||
import Link from 'next/link'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
type ForgotPasswordLayoutProps = {
|
||||
heading: string
|
||||
subheading: string
|
||||
heading?: string
|
||||
subheading?: string
|
||||
logoLinkToMarketingSite?: boolean
|
||||
showHeadings?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const ForgotPasswordLayout = ({
|
||||
@@ -16,12 +18,18 @@ const ForgotPasswordLayout = ({
|
||||
subheading,
|
||||
logoLinkToMarketingSite = false,
|
||||
showHeadings = true,
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<ForgotPasswordLayoutProps>) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex-1 bg-studio flex flex-col gap-8 lg:gap-16 xl:gap-32">
|
||||
<div
|
||||
className={cn(
|
||||
'min-h-screen flex-1 bg-studio flex flex-col gap-8 lg:gap-16 xl:gap-32',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="sticky top-0 mx-auto w-full max-w-7xl px-8 pt-6 sm:px-6 lg:px-8">
|
||||
<nav className="relative flex items-center justify-between sm:h-10">
|
||||
<div className="flex flex-shrink-0 flex-grow items-center lg:flex-grow-0">
|
||||
|
||||
@@ -42,6 +42,7 @@ const MemoizedMessage = memo(
|
||||
message,
|
||||
isLoading,
|
||||
onResults,
|
||||
onDelete,
|
||||
onEdit,
|
||||
isAfterEditedMessage,
|
||||
isBeingEdited,
|
||||
@@ -58,6 +59,7 @@ const MemoizedMessage = memo(
|
||||
resultId?: string
|
||||
results: any[]
|
||||
}) => void
|
||||
onDelete: (id: string) => void
|
||||
onEdit: (id: string) => void
|
||||
isAfterEditedMessage: boolean
|
||||
isBeingEdited: boolean
|
||||
@@ -70,6 +72,7 @@ const MemoizedMessage = memo(
|
||||
readOnly={message.role === 'user'}
|
||||
isLoading={isLoading}
|
||||
onResults={onResults}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
isAfterEditedMessage={isAfterEditedMessage}
|
||||
isBeingEdited={isBeingEdited}
|
||||
@@ -105,7 +108,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
)
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
const { ref: scrollContainerRef, isSticky, scrollToEnd } = useAutoScroll()
|
||||
const { ref: scrollContainerRef, isSticky, scrollToEnd, setIsSticky } = useAutoScroll()
|
||||
|
||||
const { aiOptInLevel, isHipaaProjectDisallowed } = useOrgAiOptInLevel()
|
||||
const showMetadataWarning =
|
||||
@@ -119,6 +122,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
const [value, setValue] = useState<string>(snap.initialInput || '')
|
||||
const [isConfirmOptInModalOpen, setIsConfirmOptInModalOpen] = useState(false)
|
||||
const [editingMessageId, setEditingMessageId] = useState<string | null>(null)
|
||||
const [isResubmitting, setIsResubmitting] = useState(false)
|
||||
|
||||
const { data: check, isSuccess } = useCheckOpenAIKeyQuery()
|
||||
const isApiKeySet = IS_PLATFORM || !!check?.hasKey
|
||||
@@ -232,7 +236,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
onFinish: handleChatFinish,
|
||||
})
|
||||
|
||||
const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming'
|
||||
const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming' || isResubmitting
|
||||
|
||||
const updateMessage = useCallback(
|
||||
({
|
||||
@@ -249,6 +253,22 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
[snap]
|
||||
)
|
||||
|
||||
const deleteMessageFromHere = useCallback(
|
||||
(messageId: string) => {
|
||||
// Find the message index in current chatMessages
|
||||
const messageIndex = chatMessages.findIndex((msg) => msg.id === messageId)
|
||||
if (messageIndex === -1) return
|
||||
|
||||
if (isChatLoading) stop()
|
||||
|
||||
snap.deleteMessagesAfter(messageId, { includeSelf: true })
|
||||
|
||||
const updatedMessages = chatMessages.slice(0, messageIndex)
|
||||
setMessages(updatedMessages)
|
||||
},
|
||||
[snap, setMessages, chatMessages, isChatLoading, stop]
|
||||
)
|
||||
|
||||
const editMessage = useCallback(
|
||||
(messageId: string) => {
|
||||
const messageIndex = chatMessages.findIndex((msg) => msg.id === messageId)
|
||||
@@ -266,9 +286,16 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
.join('') ?? ''
|
||||
setValue(textContent)
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef?.current?.focus()
|
||||
|
||||
// [Joshen] This is just to make the cursor go to the end of the text when focusing
|
||||
const val = inputRef.current.value
|
||||
inputRef.current.value = ''
|
||||
inputRef.current.value = val
|
||||
}
|
||||
}, 100)
|
||||
},
|
||||
[chatMessages, setValue]
|
||||
)
|
||||
@@ -292,6 +319,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
message={message}
|
||||
isLoading={isChatLoading && message.id === chatMessages[chatMessages.length - 1].id}
|
||||
onResults={updateMessage}
|
||||
onDelete={deleteMessageFromHere}
|
||||
onEdit={editMessage}
|
||||
isAfterEditedMessage={isAfterEditedMessage}
|
||||
isBeingEdited={isBeingEdited}
|
||||
@@ -299,7 +327,15 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
/>
|
||||
)
|
||||
}),
|
||||
[chatMessages, isChatLoading, updateMessage, editMessage, editingMessageId, cancelEdit]
|
||||
[
|
||||
chatMessages,
|
||||
isChatLoading,
|
||||
updateMessage,
|
||||
deleteMessageFromHere,
|
||||
editMessage,
|
||||
cancelEdit,
|
||||
editingMessageId,
|
||||
]
|
||||
)
|
||||
|
||||
const hasMessages = chatMessages.length > 0
|
||||
@@ -308,12 +344,9 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
const sendMessageToAssistant = (finalContent: string) => {
|
||||
if (editingMessageId) {
|
||||
// Handling when the user is in edit mode
|
||||
const messageIndex = chatMessages.findIndex((msg) => msg.id === editingMessageId)
|
||||
if (messageIndex === -1) return
|
||||
|
||||
snap.deleteMessagesAfter(editingMessageId, { includeSelf: true })
|
||||
const updatedMessages = chatMessages.slice(0, messageIndex)
|
||||
setMessages(updatedMessages)
|
||||
// delete the message(s) from the chat just like the delete button
|
||||
setIsResubmitting(true)
|
||||
deleteMessageFromHere(editingMessageId)
|
||||
setEditingMessageId(null)
|
||||
}
|
||||
|
||||
@@ -355,6 +388,14 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
setEditingMessageId(null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Keep "Thinking" visible while stopping and resubmitting during edit
|
||||
// Only clear once the new response actually starts streaming (or errors)
|
||||
if (isResubmitting && (chatStatus === 'streaming' || !!error)) {
|
||||
setIsResubmitting(false)
|
||||
}
|
||||
}, [isResubmitting, chatStatus, error])
|
||||
|
||||
// Update scroll behavior for new messages
|
||||
useEffect(() => {
|
||||
if (!isChatLoading) {
|
||||
@@ -381,6 +422,10 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [snap.open, isInSQLEditor, snippetContent])
|
||||
|
||||
useEffect(() => {
|
||||
if (chatMessages.length === 0 && !isSticky) setIsSticky(true)
|
||||
}, [chatMessages, isSticky, setIsSticky])
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
message="Something went wrong with the AI Assistant"
|
||||
@@ -699,7 +744,8 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
'z-20 [&>form>textarea]:text-base [&>form>textarea]:md:text-sm [&>form>textarea]:border-1 [&>form>textarea]:rounded-md [&>form>textarea]:!outline-none [&>form>textarea]:!ring-offset-0 [&>form>textarea]:!ring-0'
|
||||
)}
|
||||
loading={isChatLoading}
|
||||
disabled={!isApiKeySet || disablePrompts || isChatLoading}
|
||||
isEditing={!!editingMessageId}
|
||||
disabled={!isApiKeySet || disablePrompts || (isChatLoading && !editingMessageId)}
|
||||
placeholder={
|
||||
hasMessages
|
||||
? 'Ask a follow up question...'
|
||||
|
||||
@@ -41,6 +41,8 @@ export interface FormProps {
|
||||
snippetsClassName?: string
|
||||
/* Additional class name for the form wrapper */
|
||||
className?: string
|
||||
/* If currently editing an existing message */
|
||||
isEditing?: boolean
|
||||
}
|
||||
|
||||
const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
|
||||
@@ -59,6 +61,7 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
|
||||
snippetsClassName,
|
||||
includeSnippetsInMessage = false,
|
||||
className,
|
||||
isEditing = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -68,7 +71,7 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
|
||||
|
||||
const handleSubmit = (event?: FormEvent<HTMLFormElement>) => {
|
||||
if (event) event.preventDefault()
|
||||
if (!value || loading) return
|
||||
if (!value || (loading && !isEditing)) return
|
||||
|
||||
let finalMessage = value
|
||||
if (includeSnippetsInMessage && sqlSnippets && sqlSnippets.length > 0) {
|
||||
@@ -107,8 +110,8 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
|
||||
/>
|
||||
)}
|
||||
<ExpandingTextArea
|
||||
autoFocus={!isMobile}
|
||||
ref={textAreaRef}
|
||||
autoFocus={isMobile}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'text-sm pr-10 max-h-64',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogSection,
|
||||
DialogSectionSeparator,
|
||||
DialogTitle,
|
||||
} from 'ui'
|
||||
|
||||
type DeleteMessageConfirmModalProps = {
|
||||
visible: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export const DeleteMessageConfirmModal = ({
|
||||
visible,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: DeleteMessageConfirmModalProps) => {
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) onCancel()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={onOpenChange}>
|
||||
<DialogContent size="small">
|
||||
<DialogHeader padding="small">
|
||||
<DialogTitle>Delete Message</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogSectionSeparator />
|
||||
|
||||
<DialogSection padding="small">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Are you sure you want to delete this message and all subsequent messages? This action
|
||||
cannot be undone.
|
||||
</p>
|
||||
</DialogSection>
|
||||
|
||||
<DialogFooter padding="small">
|
||||
<Button type="default" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="danger" onClick={onConfirm}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { UIMessage as VercelMessage } from '@ai-sdk/react'
|
||||
import { Loader2, Pencil } from 'lucide-react'
|
||||
import { createContext, PropsWithChildren, ReactNode, useMemo } from 'react'
|
||||
import { Loader2, Pencil, Trash2 } from 'lucide-react'
|
||||
import { createContext, PropsWithChildren, ReactNode, useMemo, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { Components } from 'react-markdown/lib/ast-to-react'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { ProfileImage } from 'components/ui/ProfileImage'
|
||||
import { useProfile } from 'lib/profile'
|
||||
@@ -11,6 +12,7 @@ import { cn, markdownComponents, WarningIcon } from 'ui'
|
||||
import { ButtonTooltip } from '../ButtonTooltip'
|
||||
import { EdgeFunctionBlock } from '../EdgeFunctionBlock/EdgeFunctionBlock'
|
||||
import { DisplayBlockRenderer } from './DisplayBlockRenderer'
|
||||
import { DeleteMessageConfirmModal } from './DeleteMessageConfirmModal'
|
||||
import {
|
||||
Heading3,
|
||||
Hyperlink,
|
||||
@@ -51,6 +53,7 @@ interface MessageProps {
|
||||
resultId?: string
|
||||
results: any[]
|
||||
}) => void
|
||||
onDelete: (id: string) => void
|
||||
onEdit: (id: string) => void
|
||||
isAfterEditedMessage: boolean
|
||||
isBeingEdited: boolean
|
||||
@@ -65,12 +68,14 @@ export const Message = function Message({
|
||||
action = null,
|
||||
variant = 'default',
|
||||
onResults,
|
||||
onDelete,
|
||||
onEdit,
|
||||
isAfterEditedMessage = false,
|
||||
isBeingEdited = false,
|
||||
onCancelEdit,
|
||||
}: PropsWithChildren<MessageProps>) {
|
||||
const { profile } = useProfile()
|
||||
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false)
|
||||
const allMarkdownComponents: Partial<Components> = useMemo(
|
||||
() => ({
|
||||
...markdownComponents,
|
||||
@@ -237,30 +242,54 @@ export const Message = function Message({
|
||||
<span className="text-foreground-lighter italic">Assistant is thinking...</span>
|
||||
)}
|
||||
|
||||
{/* Action button - only show for user messages on hover */}
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{/* Action buttons - only show for user messages on hover */}
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-1">
|
||||
{message.role === 'user' && (
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
icon={<Pencil size={14} strokeWidth={1.5} />}
|
||||
onClick={isBeingEdited || isAfterEditedMessage ? onCancelEdit : () => onEdit(id)}
|
||||
className="text-foreground-light hover:text-foreground p-1 rounded"
|
||||
aria-label={
|
||||
isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message'
|
||||
}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text:
|
||||
isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
icon={<Pencil size={14} strokeWidth={1.5} />}
|
||||
onClick={
|
||||
isBeingEdited || isAfterEditedMessage ? onCancelEdit : () => onEdit(id)
|
||||
}
|
||||
className="text-foreground-light hover:text-foreground p-1 rounded"
|
||||
aria-label={
|
||||
isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message'
|
||||
}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text:
|
||||
isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
icon={<Trash2 size={14} strokeWidth={1.5} />}
|
||||
tooltip={{ content: { side: 'bottom', text: 'Delete message' } }}
|
||||
onClick={() => setShowDeleteConfirmModal(true)}
|
||||
className="text-foreground-light hover:text-foreground p-1 rounded"
|
||||
title="Delete message"
|
||||
aria-label="Delete message"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DeleteMessageConfirmModal
|
||||
visible={showDeleteConfirmModal}
|
||||
onConfirm={() => {
|
||||
onDelete(id)
|
||||
setShowDeleteConfirmModal(false)
|
||||
toast.success('Message deleted successfully')
|
||||
}}
|
||||
onCancel={() => setShowDeleteConfirmModal(false)}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -69,5 +69,5 @@ export function useAutoScroll({ enabled = true }: UseAutoScrollProps = {}) {
|
||||
}
|
||||
}, [container, enabled, scrollToEnd])
|
||||
|
||||
return { ref, isSticky, scrollToEnd }
|
||||
return { ref, isSticky, scrollToEnd, setIsSticky }
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { cn } from 'ui'
|
||||
export const GridFooter = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
|
||||
return (
|
||||
<div
|
||||
aria-label="Table grid footer"
|
||||
className={cn(
|
||||
'flex min-h-9 h-9 overflow-hidden overflow-x-auto items-center px-2 w-full border-t',
|
||||
className
|
||||
|
||||
@@ -10,10 +10,8 @@ import Panel from 'components/ui/Panel'
|
||||
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
|
||||
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { Input } from 'ui'
|
||||
import { getLastUsedAPIKeys, useLastUsedAPIKeysLogQuery } from './DisplayApiSettings.utils'
|
||||
import { ToggleLegacyApiKeysPanel } from './ToggleLegacyApiKeys'
|
||||
|
||||
export const DisplayApiSettings = ({
|
||||
showTitle = true,
|
||||
@@ -25,9 +23,6 @@ export const DisplayApiSettings = ({
|
||||
showLegacyText?: boolean
|
||||
}) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { projectSettingsShowDisableLegacyApiKeys } = useIsFeatureEnabled([
|
||||
'project_settings:show_disable_legacy_api_keys',
|
||||
])
|
||||
|
||||
const {
|
||||
data: settings,
|
||||
@@ -74,151 +69,145 @@ export const DisplayApiSettings = ({
|
||||
}, [lastUsedLogData, apiKeys])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel
|
||||
noMargin
|
||||
title={
|
||||
showTitle && (
|
||||
<div className="space-y-3">
|
||||
<h5 className="text-base">Project API Keys</h5>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your API is secured behind an API gateway which requires an API Key for every
|
||||
request.
|
||||
<br />
|
||||
You can use the keys below in the Supabase client libraries.
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-foreground-light">Retrieving API keys</p>
|
||||
</div>
|
||||
) : !canReadAPIKeys ? (
|
||||
<div className="flex items-center py-8 px-8 space-x-2">
|
||||
<AlertCircle size={16} strokeWidth={1.5} />
|
||||
<Panel
|
||||
noMargin
|
||||
title={
|
||||
showTitle && (
|
||||
<div className="space-y-3">
|
||||
<h5 className="text-base">Project API Keys</h5>
|
||||
<p className="text-sm text-foreground-light">
|
||||
You don't have permission to view API keys. These keys restricted to users with higher
|
||||
access levels.
|
||||
Your API is secured behind an API gateway which requires an API Key for every request.
|
||||
<br />
|
||||
You can use the keys below in the Supabase client libraries.
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
) : isProjectSettingsError || isJwtSecretUpdateStatusError ? (
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<AlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-foreground-light">
|
||||
{isProjectSettingsError
|
||||
? 'Failed to retrieve API keys'
|
||||
: 'Failed to update JWT secret'}
|
||||
</p>
|
||||
</div>
|
||||
) : isApiKeysEmpty || isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-foreground-light">
|
||||
{isProjectSettingsLoading || isApiKeysEmpty
|
||||
? 'Retrieving API keys'
|
||||
: 'JWT secret is being updated'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
apiKeys.map((x, i: number) => (
|
||||
<Panel.Content
|
||||
key={x.api_key}
|
||||
className={
|
||||
i >= 1 &&
|
||||
'border-t border-panel-border-interior-light [[data-theme*=dark]_&]:border-panel-border-interior-dark'
|
||||
}
|
||||
>
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
layout="horizontal"
|
||||
className="input-mono"
|
||||
// @ts-ignore
|
||||
label={
|
||||
<>
|
||||
{x.tags?.split(',').map((x, i: number) => (
|
||||
<code key={`${x}${i}`} className="text-xs text-code">
|
||||
{x}
|
||||
)
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-foreground-light">Retrieving API keys</p>
|
||||
</div>
|
||||
) : !canReadAPIKeys ? (
|
||||
<div className="flex items-center py-8 px-8 space-x-2">
|
||||
<AlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-foreground-light">
|
||||
You don't have permission to view API keys. These keys restricted to users with higher
|
||||
access levels.
|
||||
</p>
|
||||
</div>
|
||||
) : isProjectSettingsError || isJwtSecretUpdateStatusError ? (
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<AlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-foreground-light">
|
||||
{isProjectSettingsError ? 'Failed to retrieve API keys' : 'Failed to update JWT secret'}
|
||||
</p>
|
||||
</div>
|
||||
) : isApiKeysEmpty || isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
|
||||
<div className="flex items-center justify-center py-8 space-x-2">
|
||||
<Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-foreground-light">
|
||||
{isProjectSettingsLoading || isApiKeysEmpty
|
||||
? 'Retrieving API keys'
|
||||
: 'JWT secret is being updated'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
apiKeys.map((x, i: number) => (
|
||||
<Panel.Content
|
||||
key={x.api_key}
|
||||
className={
|
||||
i >= 1 &&
|
||||
'border-t border-panel-border-interior-light [[data-theme*=dark]_&]:border-panel-border-interior-dark'
|
||||
}
|
||||
>
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
layout="horizontal"
|
||||
className="input-mono"
|
||||
// @ts-ignore
|
||||
label={
|
||||
<>
|
||||
{x.tags?.split(',').map((x, i: number) => (
|
||||
<code key={`${x}${i}`} className="text-xs text-code">
|
||||
{x}
|
||||
</code>
|
||||
))}
|
||||
{x.tags === 'service_role' && (
|
||||
<>
|
||||
<code className="text-xs text-code !bg-destructive !text-white !border-destructive">
|
||||
secret
|
||||
</code>
|
||||
))}
|
||||
{x.tags === 'service_role' && (
|
||||
<>
|
||||
<code className="text-xs text-code !bg-destructive !text-white !border-destructive">
|
||||
secret
|
||||
</code>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
{x.tags === 'anon' && <code className="text-xs text-code">public</code>}
|
||||
</>
|
||||
}
|
||||
copy={canReadAPIKeys && isNotUpdatingJwtSecret}
|
||||
reveal={x.tags !== 'anon' && canReadAPIKeys && isNotUpdatingJwtSecret}
|
||||
value={
|
||||
!canReadAPIKeys
|
||||
? 'You need additional permissions to view API keys'
|
||||
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
|
||||
? 'JWT secret update failed, new API key may have issues'
|
||||
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
|
||||
? 'Updating JWT secret...'
|
||||
: x?.api_key ?? 'You need additional permissions to view API keys'
|
||||
}
|
||||
onChange={() => {}}
|
||||
descriptionText={
|
||||
x.tags === 'service_role' ? (
|
||||
<>
|
||||
This key has the ability to bypass Row Level Security. Never share it publicly.
|
||||
If leaked, generate a new JWT secret immediately.{' '}
|
||||
{showLegacyText && (
|
||||
<span>
|
||||
Prefer using{' '}
|
||||
<Link
|
||||
href={`/project/${projectRef}/settings/api-keys/new`}
|
||||
className="text-link underline"
|
||||
>
|
||||
Secret API keys
|
||||
</Link>{' '}
|
||||
instead.
|
||||
</span>
|
||||
)}
|
||||
{x.tags === 'anon' && <code className="text-xs text-code">public</code>}
|
||||
</>
|
||||
}
|
||||
copy={canReadAPIKeys && isNotUpdatingJwtSecret}
|
||||
reveal={x.tags !== 'anon' && canReadAPIKeys && isNotUpdatingJwtSecret}
|
||||
value={
|
||||
!canReadAPIKeys
|
||||
? 'You need additional permissions to view API keys'
|
||||
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
|
||||
? 'JWT secret update failed, new API key may have issues'
|
||||
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
|
||||
? 'Updating JWT secret...'
|
||||
: x?.api_key ?? 'You need additional permissions to view API keys'
|
||||
}
|
||||
onChange={() => {}}
|
||||
descriptionText={
|
||||
x.tags === 'service_role' ? (
|
||||
<>
|
||||
This key has the ability to bypass Row Level Security. Never share it
|
||||
publicly. If leaked, generate a new JWT secret immediately.{' '}
|
||||
{showLegacyText && (
|
||||
<span>
|
||||
Prefer using{' '}
|
||||
<Link
|
||||
href={`/project/${projectRef}/settings/api-keys/new`}
|
||||
className="text-link underline"
|
||||
>
|
||||
Secret API keys
|
||||
</Link>{' '}
|
||||
instead.
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
This key is safe to use in a browser if you have enabled Row Level Security
|
||||
for your tables and configured policies.{' '}
|
||||
{showLegacyText && (
|
||||
<span>
|
||||
Prefer using{' '}
|
||||
<Link
|
||||
href={`/project/${projectRef}/settings/api-keys/new`}
|
||||
className="text-link underline"
|
||||
>
|
||||
Publishable API keys
|
||||
</Link>{' '}
|
||||
instead.
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="pt-2 text-foreground-lighter w-full text-sm data-[invisible=true]:invisible"
|
||||
data-invisible={isLoadingLastUsed}
|
||||
>
|
||||
{lastUsedAPIKeys[x.api_key]
|
||||
? `Last request was ${lastUsedAPIKeys[x.api_key]} ago.`
|
||||
: 'No requests in the past 24 hours.'}
|
||||
</div>
|
||||
</Panel.Content>
|
||||
))
|
||||
)}
|
||||
</Panel>
|
||||
) : (
|
||||
<>
|
||||
This key is safe to use in a browser if you have enabled Row Level Security for
|
||||
your tables and configured policies.{' '}
|
||||
{showLegacyText && (
|
||||
<span>
|
||||
Prefer using{' '}
|
||||
<Link
|
||||
href={`/project/${projectRef}/settings/api-keys/new`}
|
||||
className="text-link underline"
|
||||
>
|
||||
Publishable API keys
|
||||
</Link>{' '}
|
||||
instead.
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="pt-2 text-foreground-lighter w-full text-sm data-[invisible=true]:invisible"
|
||||
data-invisible={isLoadingLastUsed}
|
||||
>
|
||||
{lastUsedAPIKeys[x.api_key]
|
||||
? `Last request was ${lastUsedAPIKeys[x.api_key]} ago.`
|
||||
: 'No requests in the past 24 hours.'}
|
||||
</div>
|
||||
</Panel.Content>
|
||||
))
|
||||
)}
|
||||
{showNotice ? (
|
||||
<Panel.Notice
|
||||
className="border-t"
|
||||
@@ -230,9 +219,7 @@ export const DisplayApiSettings = ({
|
||||
href="https://github.com/orgs/supabase/discussions/29260"
|
||||
buttonText="Read the announcement"
|
||||
/>
|
||||
) : projectSettingsShowDisableLegacyApiKeys ? (
|
||||
<ToggleLegacyApiKeysPanel />
|
||||
) : null}
|
||||
</>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
+7
-1
@@ -31,6 +31,8 @@ const isDevOrStaging =
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
|
||||
|
||||
const NIMBUS_STAGING_PROJECTS_URL = 'https://*.supabase-nimbus-projects.com'
|
||||
const NIMBUS_STAGING_PROJECTS_URL_WS = 'wss://*.supabase-nimbus-projects.com'
|
||||
const SUPABASE_STAGING_PROJECTS_URL = 'https://*.supabase.red'
|
||||
const SUPABASE_STAGING_PROJECTS_URL_WS = 'wss://*.supabase.red'
|
||||
const SUPABASE_COM_URL = 'https://supabase.com'
|
||||
@@ -126,6 +128,8 @@ module.exports.getCSP = function getCSP() {
|
||||
? [
|
||||
SUPABASE_STAGING_PROJECTS_URL,
|
||||
SUPABASE_STAGING_PROJECTS_URL_WS,
|
||||
NIMBUS_STAGING_PROJECTS_URL,
|
||||
NIMBUS_STAGING_PROJECTS_URL_WS,
|
||||
VERCEL_LIVE_URL,
|
||||
SUPABASE_DOCS_PROJECT_URL,
|
||||
SUPABASE_CONTENT_API_URL,
|
||||
@@ -140,7 +144,9 @@ module.exports.getCSP = function getCSP() {
|
||||
`blob:`,
|
||||
`data:`,
|
||||
...IMG_SRC_URLS,
|
||||
...(isDevOrStaging ? [SUPABASE_STAGING_PROJECTS_URL, VERCEL_URL] : []),
|
||||
...(isDevOrStaging
|
||||
? [SUPABASE_STAGING_PROJECTS_URL, NIMBUS_STAGING_PROJECTS_URL, VERCEL_URL]
|
||||
: []),
|
||||
].join(' ')
|
||||
|
||||
const scriptSrcDirective = [
|
||||
|
||||
@@ -48,7 +48,7 @@ export const useCheckGithubBranchValidity = ({
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to check Github branch: ${data.message}`)
|
||||
toast.error(`Failed to check GitHub branch: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const useGitHubConnectionCreateMutation = ({
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to create Github connection: ${data.message}`)
|
||||
toast.error(`Failed to create GitHub connection: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const useGitHubConnectionDeleteMutation = ({
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to delete Github connection: ${data.message}`)
|
||||
toast.error(`Failed to delete GitHub connection: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export const useGitHubConnectionUpdateMutation = ({
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to update Github connection: ${data.message}`)
|
||||
toast.error(`Failed to update GitHub connection: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ConnectionType } from 'components/interfaces/Connect/Connect.constants'
|
||||
|
||||
export type CustomContentTypes = {
|
||||
organizationLegalDocuments: {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
action: { text: string; url: string }
|
||||
}[]
|
||||
|
||||
projectHomepageExampleProjects: {
|
||||
title: string
|
||||
description: string
|
||||
iconUrl: string
|
||||
url: string
|
||||
}[]
|
||||
|
||||
logsDefaultQuery: string
|
||||
|
||||
/**
|
||||
* When declaring files for each framework, there are 3 properties that can be dynamically rendered into the file content using handlebar notation:
|
||||
* - {{apiUrl}}: The API URL of the project
|
||||
* - {{anonKey}}: The anonymous key of the project (if still using legacy API keys)
|
||||
* - {{publishableKey}}: The publishable API key of the project (if using new API keys)
|
||||
*
|
||||
* These could be helpful in rendering, for e.g an environment file like `.env`
|
||||
*/
|
||||
connectFrameworks: {
|
||||
key: string
|
||||
label: string
|
||||
obj: ConnectionType[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "./custom-content.schema.json",
|
||||
|
||||
"organization:legal_documents": null,
|
||||
|
||||
"project_homepage:example_projects": null,
|
||||
|
||||
"logs:default_query": null,
|
||||
|
||||
"connect:frameworks": null
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "./custom-content.schema.json",
|
||||
|
||||
"organization:legal_documents": [
|
||||
{
|
||||
"id": "doc1",
|
||||
"name": "Document 1",
|
||||
"description": "This is a description of Document 1",
|
||||
"action": {
|
||||
"text": "Download document",
|
||||
"url": "https://supabase.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "doc2",
|
||||
"name": "Document 2",
|
||||
"description": "This is a description of Document 2",
|
||||
"action": {
|
||||
"text": "Download document",
|
||||
"url": "https://supabase.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"project_homepage:example_projects": [
|
||||
{
|
||||
"title": "Framework 1",
|
||||
"description": "This is a description of Framework 1",
|
||||
"iconUrl": "https://supabase.com/dashboard/img/supabase-logo.svg",
|
||||
"url": "https://supabase.com"
|
||||
},
|
||||
{
|
||||
"title": "Framework 2",
|
||||
"description": "This is a description of Framework 2",
|
||||
"iconUrl": "https://supabase.com/dashboard/img/supabase-logo.svg",
|
||||
"url": "https://supabase.com"
|
||||
}
|
||||
],
|
||||
|
||||
"logs:default_query": "This is a sample query",
|
||||
|
||||
"connect:frameworks": {
|
||||
"key": "frameworks",
|
||||
"label": "Frameworks",
|
||||
"obj": [
|
||||
{
|
||||
"key": "framework-1",
|
||||
"label": "Framework 1",
|
||||
"icon": "https://supabase.com/dashboard/img/supabase-logo.svg",
|
||||
"guideLink": "https://supabase.com/docs",
|
||||
"children": [],
|
||||
"files": [
|
||||
{
|
||||
"name": "sample_env",
|
||||
"content": "NEXT_PUBLIC_SUPABASE_URL={{apiUrl}}\nNEXT_PUBLIC_SUPABASE_ANON_KEY={{anonKey}}"
|
||||
},
|
||||
{
|
||||
"name": "sample.ts",
|
||||
"content": "import { createClient } from \"@supabase/supabase-js\";\n\nconst supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\nconst supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\nexport const supabase = createClient(supabaseUrl, supabaseKey);"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "framework-2",
|
||||
"label": "Framework 2",
|
||||
"icon": "https://supabase.com/dashboard/img/supabase-logo.svg",
|
||||
"guideLink": "https://supabase.com/docs",
|
||||
"children": [],
|
||||
"files": [
|
||||
{ "name": "file3.tsx", "content": "Content of File 3" },
|
||||
{ "name": "file4.tsx", "content": "Content of File 4" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
"organization:legal_documents": {
|
||||
"type": ["array", "null"],
|
||||
"description": "Renders a provided set of documents under the organization legal documents page",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": { "type": "string" },
|
||||
"url": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"project_homepage:example_projects": {
|
||||
"type": ["array", "null"],
|
||||
"description": "Renders a provided set of example projects under the project's home page",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"iconUrl": { "type": "string" },
|
||||
"url": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"logs:default_query": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Renders a default query when landing on the logs explorer"
|
||||
},
|
||||
|
||||
"connect:frameworks": {
|
||||
"type": ["object", "null"],
|
||||
"description": "Replaces the frameworks tab in the Connect modal when a defined set of frameworks",
|
||||
"properties": {
|
||||
"key": { "type": "string" },
|
||||
"label": { "type": "string" },
|
||||
"obj": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": { "type": "string" },
|
||||
"icon": { "type": "string" },
|
||||
"label": { "type": "string" },
|
||||
"guideLink": { "type": "string" },
|
||||
"children": { "type": "array" },
|
||||
"files": {
|
||||
"type": "array",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"content": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"organization:legal_documents",
|
||||
"project_homepage:example_projects",
|
||||
"logs:default_query",
|
||||
"connect:frameworks"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { renderHook, cleanup } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe('useCustomContent', () => {
|
||||
it('should return null if content is not found in the custom-content.json file', async () => {
|
||||
vi.doMock('./custom-content.json', () => ({
|
||||
default: {
|
||||
'organization:legal_documents': null,
|
||||
},
|
||||
}))
|
||||
|
||||
const { useCustomContent } = await import('./useCustomContent')
|
||||
const { result } = renderHook(() => useCustomContent(['organization:legal_documents']))
|
||||
expect(result.current.organizationLegalDocuments).toEqual(null)
|
||||
})
|
||||
|
||||
it('should return the content for the key passed in if it exists in the custom-content.json file', async () => {
|
||||
vi.doMock('./custom-content.json', () => ({
|
||||
default: {
|
||||
'organization:legal_documents': {
|
||||
someValue: 'foo',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const { useCustomContent } = await import('./useCustomContent')
|
||||
const { result } = renderHook(() => useCustomContent(['organization:legal_documents']))
|
||||
expect(result.current.organizationLegalDocuments).toEqual({ someValue: 'foo' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import customContentRaw from './custom-content.json'
|
||||
import { CustomContentTypes } from './CustomContent.types'
|
||||
|
||||
// [Joshen] See if we can de-dupe any of the logic here with enabled-features
|
||||
// For now just getting something working going first
|
||||
// Also not sure if CustomContentTypes is the right way to go here with trying to dynamically type
|
||||
|
||||
const customContentStaticObj = customContentRaw as Omit<typeof customContentRaw, '$schema'>
|
||||
type CustomContent = keyof typeof customContentStaticObj
|
||||
|
||||
type SnakeToCamelCase<S extends string> = S extends `${infer First}_${infer Rest}`
|
||||
? `${First}${SnakeToCamelCase<Capitalize<Rest>>}`
|
||||
: S
|
||||
|
||||
type CustomContentToCamelCase<S extends CustomContent> = S extends `${infer P}:${infer R}`
|
||||
? `${SnakeToCamelCase<P>}${Capitalize<SnakeToCamelCase<R>>}`
|
||||
: SnakeToCamelCase<S>
|
||||
|
||||
function contentToCamelCase(feature: CustomContent) {
|
||||
return feature
|
||||
.replace(/:/g, '_')
|
||||
.split('_')
|
||||
.map((word, index) => (index === 0 ? word : word[0].toUpperCase() + word.slice(1)))
|
||||
.join('') as CustomContentToCamelCase<typeof feature>
|
||||
}
|
||||
|
||||
const useCustomContent = <T extends CustomContent[]>(
|
||||
contents: T
|
||||
): {
|
||||
[key in CustomContentToCamelCase<T[number]>]:
|
||||
| (typeof customContentStaticObj)[CustomContent]
|
||||
| CustomContentTypes[CustomContentToCamelCase<T[number]>]
|
||||
} => {
|
||||
// [Joshen] Running into some TS errors without the `as` here - must be overlooking something super simple
|
||||
return Object.fromEntries(
|
||||
contents.map((content) => [contentToCamelCase(content), customContentStaticObj[content]])
|
||||
) as {
|
||||
[key in CustomContentToCamelCase<T[number]>]:
|
||||
| (typeof customContentStaticObj)[CustomContent]
|
||||
| CustomContentTypes[CustomContentToCamelCase<T[number]>]
|
||||
}
|
||||
}
|
||||
|
||||
export { useCustomContent }
|
||||
@@ -112,7 +112,7 @@ Sentry.init({
|
||||
// [Joshen] Seems to be from hcaptcha
|
||||
"undefined is not an object (evaluating 'n.chat.setReady')",
|
||||
"undefined is not an object (evaluating 'i.chat.setReady')",
|
||||
// [Terry] When users paste in an embedded Github Gist
|
||||
// [Terry] When users paste in an embedded GitHub Gist
|
||||
// Error thrown by `sql-formatter` lexer when given invalid input
|
||||
// Original format: new Error(`Parse error: Unexpected "${text}" at line ${line} column ${col}`)
|
||||
/^Parse error: Unexpected ".+" at line \d+ column \d+$/,
|
||||
@@ -152,3 +152,6 @@ function standardiseRouterUrl(url: string) {
|
||||
|
||||
return finalUrl
|
||||
}
|
||||
|
||||
// This export will instrument router navigations, and is only relevant if you enable tracing.
|
||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
|
||||
@@ -24,7 +24,7 @@ export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<{ commitSha: string; commitTime: string }>
|
||||
) {
|
||||
// Set cache control headers for 10 minutes so that we don't get banned by Github API
|
||||
// Set cache control headers for 10 minutes so that we don't get banned by GitHub API
|
||||
res.setHeader('Cache-Control', 's-maxage=600')
|
||||
|
||||
// Get the build commit SHA from Vercel environment variable
|
||||
|
||||
@@ -3,8 +3,9 @@ import Link from 'next/link'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { ClientLibrary, ExampleProject } from 'components/interfaces/Home'
|
||||
import { ClientLibrary } from 'components/interfaces/Home'
|
||||
import { AdvisorWidget } from 'components/interfaces/Home/AdvisorWidget'
|
||||
import { ExampleProject } from 'components/interfaces/Home/ExampleProject'
|
||||
import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
|
||||
import { NewProjectPanel } from 'components/interfaces/Home/NewProjectPanel/NewProjectPanel'
|
||||
import { ProjectUsageSection } from 'components/interfaces/Home/ProjectUsageSection'
|
||||
@@ -19,6 +20,7 @@ import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
|
||||
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import {
|
||||
@@ -50,6 +52,8 @@ const Home: NextPageWithLayout = () => {
|
||||
const snap = useAppStateSnapshot()
|
||||
const { ref, enableBranching } = useParams()
|
||||
|
||||
const { projectHomepageExampleProjects } = useCustomContent(['project_homepage:example_projects'])
|
||||
|
||||
const {
|
||||
projectHomepageShowAllClientLibraries: showAllClientLibraries,
|
||||
projectHomepageShowInstanceSize: showInstanceSize,
|
||||
@@ -243,36 +247,46 @@ const Home: NextPageWithLayout = () => {
|
||||
</div>
|
||||
</div>
|
||||
{showExamples && (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<h4 className="text-lg">Example projects</h4>
|
||||
<div className="flex justify-center">
|
||||
<Tabs_Shadcn_ defaultValue="app" className="w-full">
|
||||
<TabsList_Shadcn_ className="flex gap-4 mb-8">
|
||||
<TabsTrigger_Shadcn_ value="app">App Frameworks</TabsTrigger_Shadcn_>
|
||||
<TabsTrigger_Shadcn_ value="mobile">
|
||||
Mobile Frameworks
|
||||
</TabsTrigger_Shadcn_>
|
||||
</TabsList_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="app">
|
||||
<div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{EXAMPLE_PROJECTS.filter((project) => project.type === 'app')
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.map((project) => (
|
||||
<ExampleProject key={project.url} {...project} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="mobile">
|
||||
<div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{EXAMPLE_PROJECTS.filter((project) => project.type === 'mobile')
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.map((project) => (
|
||||
<ExampleProject key={project.url} {...project} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
</Tabs_Shadcn_>
|
||||
</div>
|
||||
{!!projectHomepageExampleProjects ? (
|
||||
<div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{projectHomepageExampleProjects
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.map((project) => (
|
||||
<ExampleProject key={project.url} {...project} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center">
|
||||
<Tabs_Shadcn_ defaultValue="app" className="w-full">
|
||||
<TabsList_Shadcn_ className="flex gap-4 mb-8">
|
||||
<TabsTrigger_Shadcn_ value="app">App Frameworks</TabsTrigger_Shadcn_>
|
||||
<TabsTrigger_Shadcn_ value="mobile">
|
||||
Mobile Frameworks
|
||||
</TabsTrigger_Shadcn_>
|
||||
</TabsList_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="app">
|
||||
<div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{EXAMPLE_PROJECTS.filter((project) => project.type === 'app')
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.map((project) => (
|
||||
<ExampleProject key={project.url} {...project} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="mobile">
|
||||
<div className="grid gap-2 md:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{EXAMPLE_PROJECTS.filter((project) => project.type === 'mobile')
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.map((project) => (
|
||||
<ExampleProject key={project.url} {...project} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
</Tabs_Shadcn_>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from 'data/content/content-upsert-mutation'
|
||||
import useLogsQuery from 'hooks/analytics/useLogsQuery'
|
||||
import { useLogsUrlState } from 'hooks/analytics/useLogsUrlState'
|
||||
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
@@ -57,8 +58,6 @@ const LOCAL_PLACEHOLDER_QUERY =
|
||||
const PLATFORM_PLACEHOLDER_QUERY =
|
||||
'select\n cast(timestamp as datetime) as timestamp,\n event_message, metadata \nfrom edge_logs \nlimit 5'
|
||||
|
||||
const PLACEHOLDER_QUERY = IS_PLATFORM ? PLATFORM_PLACEHOLDER_QUERY : LOCAL_PLACEHOLDER_QUERY
|
||||
|
||||
export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
useEditorHints()
|
||||
const monaco = useMonaco()
|
||||
@@ -72,6 +71,11 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
const [editorId] = useState<string>(uuidv4())
|
||||
const { timestampStart, timestampEnd, setTimeRange } = useLogsUrlState()
|
||||
|
||||
const { logsDefaultQuery } = useCustomContent(['logs:default_query'])
|
||||
const PLACEHOLDER_QUERY = IS_PLATFORM
|
||||
? logsDefaultQuery ?? PLATFORM_PLACEHOLDER_QUERY
|
||||
: LOCAL_PLACEHOLDER_QUERY
|
||||
|
||||
const [editorValue, setEditorValue] = useState<string>(PLACEHOLDER_QUERY)
|
||||
const [saveModalOpen, setSaveModalOpen] = useState<boolean>(false)
|
||||
const [warnings, setWarnings] = useState<LogsWarning[]>([])
|
||||
|
||||
@@ -2,10 +2,21 @@ import ApiKeysLayout from 'components/layouts/APIKeys/APIKeysLayout'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
|
||||
import { DisplayApiSettings } from 'components/ui/ProjectSettings'
|
||||
import { ToggleLegacyApiKeysPanel } from 'components/ui/ProjectSettings/ToggleLegacyApiKeys'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const ApiKeysLegacyPage: NextPageWithLayout = () => {
|
||||
return <DisplayApiSettings showTitle={false} showNotice={false} />
|
||||
const { projectSettingsShowDisableLegacyApiKeys } = useIsFeatureEnabled([
|
||||
'project_settings:show_disable_legacy_api_keys',
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<DisplayApiSettings showTitle={false} showNotice={false} />
|
||||
{projectSettingsShowDisableLegacyApiKeys && <ToggleLegacyApiKeysPanel />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ApiKeysLegacyPage.getLayout = (page) => (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import ResetPasswordForm from 'components/interfaces/SignIn/ResetPasswordForm'
|
||||
import ForgotPasswordLayout from 'components/layouts/SignInLayout/ForgotPasswordLayout'
|
||||
import { withAuth } from 'hooks/misc/withAuth'
|
||||
@@ -7,11 +5,9 @@ import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const ResetPasswordPage: NextPageWithLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ResetPasswordForm />
|
||||
</div>
|
||||
</>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ResetPasswordForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useIsLoggedIn } from 'common'
|
||||
import { useOrganizationByFlyOrgIdMutation } from 'data/organizations/organization-by-fly-organization-id-mutation'
|
||||
import { useProjectByFlyExtensionIdMutation } from 'data/projects/project-by-fly-extension-id-mutation'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { API_URL, BASE_PATH } from 'lib/constants'
|
||||
import { Button } from 'ui'
|
||||
|
||||
const SignInFlyTos = () => {
|
||||
@@ -78,8 +78,8 @@ const SignInFlyTos = () => {
|
||||
<Image
|
||||
src={
|
||||
resolvedTheme?.includes('dark')
|
||||
? `${router.basePath}/img/supabase-dark.svg`
|
||||
: `${router.basePath}/img/supabase-light.svg`
|
||||
? `${BASE_PATH}/img/supabase-dark.svg`
|
||||
: `${BASE_PATH}/img/supabase-light.svg`
|
||||
}
|
||||
alt=""
|
||||
height={24}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { SignInPartner } from 'components/interfaces/SignIn/SignInPartner'
|
||||
import ForgotPasswordLayout from 'components/layouts/SignInLayout/ForgotPasswordLayout'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { cn } from 'ui'
|
||||
|
||||
const SignInPartnerPage: NextPageWithLayout = () => {
|
||||
return <SignInPartner />
|
||||
}
|
||||
|
||||
SignInPartnerPage.getLayout = (page) => (
|
||||
// [Joshen] Just using this layout for the styling
|
||||
<ForgotPasswordLayout
|
||||
showHeadings={false}
|
||||
className={cn(
|
||||
'mx-auto max-w-7xl px-8 sm:px-12 lg:px-16 !gap-y-0',
|
||||
'[&>div:first-child]:absolute [&>div:first-child]:!px-0',
|
||||
'[&>div:last-child]:flex-grow [&>div:last-child>main]:!px-0'
|
||||
)}
|
||||
>
|
||||
{page}
|
||||
</ForgotPasswordLayout>
|
||||
)
|
||||
|
||||
export default SignInPartnerPage
|
||||
@@ -23,7 +23,7 @@ const SocialAuthDemo = () => {
|
||||
<div className="flex flex-col gap-6">
|
||||
{error && <p className="text-sm text-destructive-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Logging in...' : 'Continue with Github'}
|
||||
{isLoading ? 'Logging in...' : 'Continue with GitHub'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Social authentication block for React Router
|
||||
<BlockPreview name="social-auth/auth/login" />
|
||||
|
||||
<Callout className="mt-4">
|
||||
The block is using Github provider by default, but can be easily switched by changing a single
|
||||
The block is using GitHub provider by default, but can be easily switched by changing a single
|
||||
parameter.
|
||||
</Callout>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Social authentication block for React Single Page Applications
|
||||
<BlockPreview name="social-auth/auth/login" />
|
||||
|
||||
<Callout className="mt-4">
|
||||
The block is using Github provider by default, but can be easily switched by changing a single
|
||||
The block is using GitHub provider by default, but can be easily switched by changing a single
|
||||
parameter.
|
||||
</Callout>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Social authentication block for Tanstack Start
|
||||
<BlockPreview name="social-auth/auth/login" />
|
||||
|
||||
<Callout className="mt-4">
|
||||
The block is using Github provider by default, but can be easily switched by changing a single
|
||||
The block is using GitHub provider by default, but can be easily switched by changing a single
|
||||
parameter.
|
||||
</Callout>
|
||||
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe
|
||||
<div className="flex flex-col gap-6">
|
||||
{error && <p className="text-sm text-destructive-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Logging in...' : 'Continue with Github'}
|
||||
{isLoading ? 'Logging in...' : 'Continue with GitHub'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ export default function Login() {
|
||||
<div className="flex flex-col gap-6">
|
||||
{error && <p className="text-sm text-destructive-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? 'Logging in...' : 'Continue with Github'}
|
||||
{loading ? 'Logging in...' : 'Continue with GitHub'}
|
||||
</Button>
|
||||
</div>
|
||||
</fetcher.Form>
|
||||
|
||||
@@ -47,7 +47,7 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe
|
||||
<div className="flex flex-col gap-6">
|
||||
{error && <p className="text-sm text-destructive-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Logging in...' : 'Continue with Github'}
|
||||
{isLoading ? 'Logging in...' : 'Continue with GitHub'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe
|
||||
<div className="flex flex-col gap-6">
|
||||
{error && <p className="text-sm text-destructive-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Logging in...' : 'Continue with Github'}
|
||||
{isLoading ? 'Logging in...' : 'Continue with GitHub'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -33,7 +33,7 @@ Before we dive in, let's look at some prerequisites you'll need:
|
||||
|
||||
## Loading data into Supabase using Python
|
||||
|
||||
Supabase is built for developers, and you can [get started for free](https://supabase.com/dashboard) using your existing Github account. Once your Supabase account is set up, you will access the Supabase dashboard. From here, go to All Project > New Project.
|
||||
Supabase is built for developers, and you can [get started for free](https://supabase.com/dashboard) using your existing GitHub account. Once your Supabase account is set up, you will access the Supabase dashboard. From here, go to All Project > New Project.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ In order to build this chat app, we will need a database to store all of the cha
|
||||
|
||||
This article also assumes that you have installed Flutter already on your machine. If not, you can go to the [official Flutter page](https://docs.flutter.dev/get-started/install) to start the installation.
|
||||
|
||||
You can find the complete version of this app in [this](https://github.com/supabase-community/flutter-chat) Github repository.
|
||||
You can find the complete version of this app in [this](https://github.com/supabase-community/flutter-chat) GitHub repository.
|
||||
|
||||
## Overview of the final chat app
|
||||
|
||||
@@ -74,7 +74,7 @@ Run `flutter pub get` to install the packages. Note that you will have to termin
|
||||
|
||||
If you do not have a Supabase account yet, do not worry, you can [get started for free](https://supabase.com/dashboard/).
|
||||
|
||||
You will be prompted to sign in using your Github account with a big green button, so let's go ahead and press it. Proceed with the sign up process and once you are done, you will be taken to a list of projects. You can go ahead and create a new project by pressing the “New Project” button at the top.
|
||||
You will be prompted to sign in using your GitHub account with a big green button, so let's go ahead and press it. Proceed with the sign up process and once you are done, you will be taken to a list of projects. You can go ahead and create a new project by pressing the “New Project” button at the top.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Supabase Flutter SDK is getting a major update, with a big focus on developer ex
|
||||
|
||||

|
||||
|
||||
We've been hard at work making an Auth experience that is as smooth as possible for Supabase developers. The new Auth Helpers have a better experience for server-side rendering (SSR) environments, new Github structure, SvelteKit support, and more. [Read the blog](https://supabase.com/blog/supabase-auth-helpers-with-sveltekit-support).
|
||||
We've been hard at work making an Auth experience that is as smooth as possible for Supabase developers. The new Auth Helpers have a better experience for server-side rendering (SSR) environments, new GitHub structure, SvelteKit support, and more. [Read the blog](https://supabase.com/blog/supabase-auth-helpers-with-sveltekit-support).
|
||||
|
||||
## pg_jsonschema – A Postgres extension for JSON validation
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ It is also much easier to become-and remain-SOC2 compliant when you institute th
|
||||
|
||||
## Choosing a compliance monitoring tool
|
||||
|
||||
There are a few tools that plug into your cloud providers, task management system, identity, and HR providers and make it easy to monitor, collect and submit evidence to auditors. We evaluated [Vanta](https://www.vanta.com/), [Drata](https://drata.com/), [Secureframe](https://secureframe.com/), and [Tugboat Logic](https://tugboatlogic.com/). Our stack is pretty standard (AWS, GCP, GSuite, and Github). All the tools we evaluated were integrated with these systems.
|
||||
There are a few tools that plug into your cloud providers, task management system, identity, and HR providers and make it easy to monitor, collect and submit evidence to auditors. We evaluated [Vanta](https://www.vanta.com/), [Drata](https://drata.com/), [Secureframe](https://secureframe.com/), and [Tugboat Logic](https://tugboatlogic.com/). Our stack is pretty standard (AWS, GCP, GSuite, and GitHub). All the tools we evaluated were integrated with these systems.
|
||||
|
||||
Some of them had overly broad permissions when integrating with our vendors (read/write access to Github for example) which made them a no-go. Some came with a lightweight agent that we could use for Mobile Device Management (MDM). Other than that, the products are largely the same.
|
||||
Some of them had overly broad permissions when integrating with our vendors (read/write access to GitHub for example) which made them a no-go. Some came with a lightweight agent that we could use for Mobile Device Management (MDM). Other than that, the products are largely the same.
|
||||
|
||||
We chose Vanta, with Drata coming in a close second. Vanta supported more compliance standards (at that time) and is a fellow YC company (which sealed the deal). The gap between these tools is much closer now since all of them support the compliance standards we have our eyes on.
|
||||
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ Along our jurney we will:
|
||||
</div>
|
||||
Watch the video version of the tutorial.
|
||||
|
||||
Since there are quite some code snippets we need I've put together the [full source code on Github](https://github.com/saimon24/supabase-trello-angular) so you can easily run the project yourself!
|
||||
Since there are quite some code snippets we need I've put together the [full source code on GitHub](https://github.com/saimon24/supabase-trello-angular) so you can easily run the project yourself!
|
||||
|
||||
If you are not familiar with [Trello](https://trello.com/), it's a way to manage projects with different boards, lists and cards!
|
||||
|
||||
@@ -1384,7 +1384,7 @@ With that final piece of code we are completely done with our Supabase Angular T
|
||||
|
||||
Building projects with Supabase is awesome, and hopefully this real world clone example gave you insight into different areas that you need to think about.
|
||||
|
||||
You can [find the full code of this tutorial on Github](https://github.com/saimon24/supabase-trello-angular) where you just need to insert your own Supabase instance and then create the tables with the included SQL file.
|
||||
You can [find the full code of this tutorial on GitHub](https://github.com/saimon24/supabase-trello-angular) where you just need to insert your own Supabase instance and then create the tables with the included SQL file.
|
||||
|
||||
If you enjoyed the tutorial, you can [find many more tutorials on my YouTube channel](https://www.youtube.com/simongrimmdev_) where I help web developers build awesome mobile apps.
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ It's pretty rare for a company to stop feature development altogether, but lucki
|
||||
|
||||
We saw a lot of progress across our Open Issues - closing over 250 issues and 50 Pull Requests.
|
||||
|
||||

|
||||

|
||||
|
||||
and we shipped a few things:
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ If you are not yet familiar with Ionic you can check out the [Ionic Quickstart g
|
||||
|
||||
However, most of the logic is Angular based and therefore applies to any Angular web project as well.
|
||||
|
||||
You can [find the full code of this tutorial on Github](https://github.com/saimon24/supa-chat) where you just need to insert your own Supabase instance and then create the tables with the included SQL file.
|
||||
You can [find the full code of this tutorial on GitHub](https://github.com/saimon24/supa-chat) where you just need to insert your own Supabase instance and then create the tables with the included SQL file.
|
||||
|
||||
But before we dive into the app, let's set up our Supabase project!
|
||||
|
||||
@@ -1316,7 +1316,7 @@ ion-textarea {
|
||||
}
|
||||
```
|
||||
|
||||
You can [find the full code of this tutorial on Github](https://github.com/saimon24/supa-chat) including the file for that pattern so your page looks almost like WhatsApp!
|
||||
You can [find the full code of this tutorial on GitHub](https://github.com/saimon24/supa-chat) including the file for that pattern so your page looks almost like WhatsApp!
|
||||
|
||||

|
||||
|
||||
@@ -1517,7 +1517,7 @@ This was the last missing piece, and now you even got seamless Supabase authenti
|
||||
|
||||
We've come a long way and covered everything from setting up tables, to defining policies to protect data, and handling authentication in Ionic Angular applications.
|
||||
|
||||
You can [find the full code of this tutorial on Github](https://github.com/saimon24/supa-chat) where you just need to insert your own Supabase instance and then create the tables with the included SQL file, plus updating the authentication settings as we did in the tutorial
|
||||
You can [find the full code of this tutorial on GitHub](https://github.com/saimon24/supa-chat) where you just need to insert your own Supabase instance and then create the tables with the included SQL file, plus updating the authentication settings as we did in the tutorial
|
||||
|
||||
Although we can now use magic link auth, something probably even better fitting for native apps would be [phone auth with Twilio](https://supabase.com/docs/guides/auth/phone-login/twilio) that's also easily possible with Supabase - just like tons of other authentication providers!
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ More than 30 creators participated in creating content in different formats and
|
||||
|
||||
- [A quick look at Supabase](https://twitter.com/Rorstro/status/1600047388852293632) by Ania Kubów.
|
||||
- [Live streaming: Supabase and Sveltekit](https://www.youtube.com/@JamesQQuick/streams) by James Q Quick.
|
||||
- [Perform Database Migration Using Github Actions & Supabase CLI](https://www.youtube.com/watch?v=iCkdtXSeq7A) by Kunal Kushwaha.
|
||||
- [Perform Database Migration Using GitHub Actions & Supabase CLI](https://www.youtube.com/watch?v=iCkdtXSeq7A) by Kunal Kushwaha.
|
||||
- [Supabase Authentication & Realtime Database with Flutter](https://www.youtube.com/watch?v=UTSSYPOd5A0) by Pooja Bhaumik.
|
||||
- [Why I Switched From AWS Cognito To Supabase The Week Before My Startup Launched](https://medium.com/@sleeplessfox/why-i-switched-from-aws-cognito-to-supabase-the-week-before-my-startup-launched-67fdc1eccb8c) by Danilo à Tellinghusen.
|
||||
- [Supabase Crash Course For Python Developers](https://www.youtube.com/watch?v=M6cfT2pqpSc) by Patrick Loeber.
|
||||
|
||||
@@ -93,7 +93,7 @@ Two key features to level up your projects are now available: Custom domains all
|
||||
|
||||
It was great to see so many high-quality projects. The decision wasn't easy, but in the end, the panel of judges chose Depulso as the winner of the Best Overall Project.
|
||||
|
||||
Congratulations 👏 to [@\_iamkarthikeyan](https://twitter.com/_iamkarthikeyan) and [@SindhuMansi](https://twitter.com/SindhuMansi) who get each a Supabase Darkmode Keyboard and the $1500 Github sponsorship!
|
||||
Congratulations 👏 to [@\_iamkarthikeyan](https://twitter.com/_iamkarthikeyan) and [@SindhuMansi](https://twitter.com/SindhuMansi) who get each a Supabase Darkmode Keyboard and the $1500 GitHub sponsorship!
|
||||
|
||||
- [Full list of Winners](https://supabase.com/blog/launch-week-6-hackathon-winners)
|
||||
- [See all the submissions](https://www.madewithsupabase.com/launch-week-6)
|
||||
@@ -107,7 +107,7 @@ Congratulations 👏 to [@\_iamkarthikeyan](https://twitter.com/_iamkarthikeyan)
|
||||
- Build any Front-end like Admin Panel or CRM for your Supabase. [Article](https://blog.jetadmin.io/how-to-build-any-front-end-such-as-admin-panel-or-crm-for-your-supabase-in-minutes/)
|
||||
- Everything Svelte: a course with all you need to know to build a modern web application. [Course](https://www.everythingsvelte.com/)
|
||||
- Edgy Edge Functions: a look at a new Edge Function per week. [Youtube Playlist](https://www.youtube.com/playlist?list=PL5S4mPUpp4OulD3olUW8Eq1IYKpUbk5Ob)
|
||||
- Pixels, an app that provides canvas to create pixel art. [App](https://pixels.refine.dev/) | [Github Repo](https://github.com/refinedev/refine/tree/next/examples/pixels)
|
||||
- Pixels, an app that provides canvas to create pixel art. [App](https://pixels.refine.dev/) | [GitHub Repo](https://github.com/refinedev/refine/tree/next/examples/pixels)
|
||||
- Supabase Angular authentication with RxJS Observables. [Tutorial](https://dev.to/kylerummens/supabase-angular-authentication-with-rxjs-observables-3d0f)
|
||||
- Create Calendar Events in React With Google Calendar API and Supabase. [Video Tutorial](https://www.youtube.com/watch?v=tgcCl52EN84)
|
||||
- Going Serverless with Supabase. [Part 1](https://dev.to/davepar/going-serverless-with-supabase-103h)
|
||||
|
||||
@@ -82,11 +82,11 @@ We teamed up with DigitalOcean to level up self-hosted Supabase. This guide show
|
||||
|
||||
[Get started](https://docs.digitalocean.com/developer-center/hosting-supabase-on-digitalocean/).
|
||||
|
||||
## 45k Github stars (and counting!)
|
||||
## 45k GitHub stars (and counting!)
|
||||
|
||||

|
||||

|
||||
|
||||
We hit two big milestones: 45k stars and Supabase is one the top 200 most-popular repos on Github 💥
|
||||
We hit two big milestones: 45k stars and Supabase is one the top 200 most-popular repos on GitHub 💥
|
||||
|
||||
Massive thanks to the community for all the support 👁️⚡️👁️
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ In this tutorial you will learn to:
|
||||
- Display a [Capacitor Google Map](https://capacitorjs.com/docs/apis/google-maps) with a marker
|
||||
- Upload files to Supabase Storage and use [image transformations](https://supabase.com/docs/guides/storage/serving/image-transformations)
|
||||
|
||||
Since there are quite some code snippets we need I've put together the [full source code on Github](https://github.com/saimon24/supabase-postgis-ionic-angular) so you can easily run the project yourself!
|
||||
Since there are quite some code snippets we need I've put together the [full source code on GitHub](https://github.com/saimon24/supabase-postgis-ionic-angular) so you can easily run the project yourself!
|
||||
|
||||
Ready for some action?
|
||||
|
||||
@@ -1030,7 +1030,7 @@ I was fascinated by the power of this simple PostGIS extension that we enabled w
|
||||
Building apps based on geolocation data is a very common scenario, and with PostGIS we can build these applications easily on the back of a [Supabase](https://supabase.com/) database (and [auth](https://supabase.com/blog/authentication-in-ionic-angular)
|
||||
), and storage, and so much more..)
|
||||
|
||||
You can [find the full code of this tutorial on Github](https://github.com/saimon24/supabase-postgis-ionic-angular) where you just need to insert your own Supabase instance. your Google Maps key and then create the tables with the included SQL file.
|
||||
You can [find the full code of this tutorial on GitHub](https://github.com/saimon24/supabase-postgis-ionic-angular) where you just need to insert your own Supabase instance. your Google Maps key and then create the tables with the included SQL file.
|
||||
|
||||
If you enjoyed the tutorial, you can [find many more tutorials and courses on Galaxies.dev](https://galaxies.dev) where I help modern web and mobile developers build epic apps 🚀
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ In this tutorial, you will learn to:
|
||||
- Work with Expo Router v2 and protected routes
|
||||
- Upload files to [Supabase Storage](https://supabase.com/docs/guides/storage)
|
||||
|
||||
You can also directly check out the [full source code on Github](https://github.com/saimon24/react-native-resumable-upload-supabase) so you can get started with Supabase fast!
|
||||
You can also directly check out the [full source code on GitHub](https://github.com/saimon24/react-native-resumable-upload-supabase) so you can get started with Supabase fast!
|
||||
|
||||
Before we get into the app, let's quickly [start a new Supabase project](https://supabase.com/dashboard).
|
||||
|
||||
@@ -606,7 +606,7 @@ It's almost too easy to use Supabase Storage, and it's a great way to store file
|
||||
|
||||
You now have a fully functional image gallery app with React Native and Supabase Storage including user authentication without writing a line of backend code!
|
||||
|
||||
You can [find the full code of this tutorial on Github](https://github.com/saimon24/react-native-resumable-upload-supabase) where you just need to insert your own Supabase URL and API key.
|
||||
You can [find the full code of this tutorial on GitHub](https://github.com/saimon24/react-native-resumable-upload-supabase) where you just need to insert your own Supabase URL and API key.
|
||||
|
||||
If you enjoyed the tutorial, you can [learn React Native on Galaxies.dev](https://galaxies.dev) where I help developers build awesome React Native apps.
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ Along with these huge AI features, we also added a bunch of new improvements els
|
||||
|
||||
For a while now, many Supabase users have been using [Zernonia's](https://github.com/zernonia) [Supabase Schema visualization tool](https://supabase-schema.vercel.app/). While this was an amazing tool, many users wanted to see something like this directly integrated into the Studio.
|
||||
|
||||
We opened an [Issue for it on Github](https://github.com/supabase/supabase/issues/15585) and within a day or two the wheels were in motion. After a couple of weeks, the feature was polished up and merged. It's inspiring to see the power of open source at work. This feature wasn't trivial, and to see community members take it from feature request to production in just a couple of weeks is mind-blowing. Unquestionably, we have one of the best open source communities out there. Huge thanks to [kamilogorek](https://github.com/kamilogorek) and [adiologydev](https://github.com/adiologydev) for their work on this!
|
||||
We opened an [Issue for it on GitHub](https://github.com/supabase/supabase/issues/15585) and within a day or two the wheels were in motion. After a couple of weeks, the feature was polished up and merged. It's inspiring to see the power of open source at work. This feature wasn't trivial, and to see community members take it from feature request to production in just a couple of weeks is mind-blowing. Unquestionably, we have one of the best open source communities out there. Huge thanks to [kamilogorek](https://github.com/kamilogorek) and [adiologydev](https://github.com/adiologydev) for their work on this!
|
||||
|
||||
Special thanks as well to [Zernonia](https://twitter.com/zernonia) for providing the inspiration for this great new feature! OSS FTW.
|
||||
|
||||
|
||||
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user