Merge branch 'master' into chore/update-brand-colors
No files matched your search
@@ -663,10 +663,23 @@ export const database: NavMenuConstant = {
|
||||
name: 'RUM: inverted index for full-text search',
|
||||
url: '/guides/database/extensions/rum',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Foreign Data Wrappers',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/database/extensions/wrappers/overview' },
|
||||
{ name: 'Connecting to Airtable', url: '/guides/database/extensions/wrappers/airtable' },
|
||||
{ name: 'Connecting to AWS S3', url: '/guides/database/extensions/wrappers/s3' },
|
||||
{ name: 'Connecting to BigQuery', url: '/guides/database/extensions/wrappers/bigquery' },
|
||||
{
|
||||
name: 'wrappers: 3rd Party Integrations',
|
||||
url: '/guides/database/extensions/wrappers',
|
||||
name: 'Connecting to ClickHouse',
|
||||
url: '/guides/database/extensions/wrappers/clickhouse',
|
||||
},
|
||||
{ name: 'Connecting to Firebase', url: '/guides/database/extensions/wrappers/firebase' },
|
||||
{ name: 'Connecting to Logflare', url: '/guides/database/extensions/wrappers/logflare' },
|
||||
{ name: 'Connecting to Stripe', url: '/guides/database/extensions/wrappers/stripe' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Content, Paragraph, Parent } from 'mdast'
|
||||
import { MdxJsxFlowElement } from 'mdast-util-mdx'
|
||||
import { Node } from 'unist'
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
/**
|
||||
* Transforms PyMdown Tabs to Supabase Tabs.
|
||||
*
|
||||
* https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/
|
||||
*/
|
||||
const remarkPyMdownTabs = function () {
|
||||
return function transformer(root: Parent) {
|
||||
visit(root, 'paragraph', (node: Paragraph, nodeIndex: number, parent: Parent) => {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const child = node.children[i]
|
||||
if (child.type !== 'text') {
|
||||
continue
|
||||
}
|
||||
|
||||
// Look for 3 '=', followed by an optionally quoted title,
|
||||
// followed by optional newlines of text
|
||||
const match = child.value.match(/^\n*=== ("?)(.+)\1\n?((?:.|\n)*)/)
|
||||
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract the tab title along with the remaining text
|
||||
const [, , title, value] = match
|
||||
|
||||
// Rewrite the node's value to remove the tab syntax
|
||||
child.value = value
|
||||
|
||||
// Extract sibling nodes that should be linked to this tab
|
||||
const siblingsToNest = extractLinkedSiblings(parent, node, nodeIndex)
|
||||
|
||||
const children: any[] = [...node.children.slice(i), ...siblingsToNest]
|
||||
|
||||
const tabPanelElement: MdxJsxFlowElement = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'TabPanel',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'id',
|
||||
value: title,
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'label',
|
||||
value: title,
|
||||
},
|
||||
],
|
||||
children,
|
||||
}
|
||||
|
||||
let nodesAdded = 0
|
||||
const previousNode = parent.children[nodeIndex - 1]
|
||||
|
||||
if (previousNode?.type === 'mdxJsxFlowElement' && previousNode.name === 'Tabs') {
|
||||
// Add TabPanel to existing Tabs component
|
||||
previousNode.children.push(tabPanelElement)
|
||||
|
||||
// Remove this node
|
||||
parent.children.splice(nodeIndex, 1)
|
||||
nodesAdded--
|
||||
} else {
|
||||
// Create new Tabs components and add TabPanel
|
||||
const tabsElement: MdxJsxFlowElement = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'Tabs',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'scrollable',
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'size',
|
||||
value: 'small',
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'type',
|
||||
value: 'underlined',
|
||||
},
|
||||
],
|
||||
children: [tabPanelElement],
|
||||
}
|
||||
|
||||
// Overwrite this paragraph node with Tabs component
|
||||
parent.children.splice(nodeIndex, 1, tabsElement)
|
||||
}
|
||||
|
||||
// If this wasn't the first child of the paragraph, create
|
||||
// a new paragraph before this with the previous text children
|
||||
if (i > 0) {
|
||||
const previousChildren = node.children.slice(0, i)
|
||||
const paragraph: Paragraph = {
|
||||
type: 'paragraph',
|
||||
children: previousChildren,
|
||||
}
|
||||
parent.children.splice(nodeIndex, 0, paragraph)
|
||||
|
||||
nodesAdded++
|
||||
}
|
||||
|
||||
// Return the correct index for the next visit, since
|
||||
// we may have added or removed an element in the array
|
||||
return nodeIndex + nodesAdded
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies sibling nodes that should be linked to this admonition
|
||||
* based on their indent level (ie. 4 spaces).
|
||||
*
|
||||
* Iterates through proceeding siblings until one is found that is
|
||||
* not indented relative to the original node.
|
||||
*
|
||||
* Splices the discovered siblings out of the original parent and returns them.
|
||||
*/
|
||||
function extractLinkedSiblings(parent: Parent, node: Node, index: number, indentAmount = 4) {
|
||||
const { column } = node.position.start
|
||||
|
||||
let nextSibling: Content
|
||||
let i = index
|
||||
|
||||
do {
|
||||
nextSibling = parent.children[++i]
|
||||
} while (nextSibling?.position && nextSibling.position.start.column === column + indentAmount)
|
||||
|
||||
return parent.children.splice(index + 1, i - index - 1)
|
||||
}
|
||||
|
||||
export default remarkPyMdownTabs
|
||||
@@ -97,6 +97,7 @@
|
||||
"remark-admonitions": "^1.2.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-slug": "^7.0.1",
|
||||
"remark-emoji": "^3.1.2",
|
||||
"shiki": "^0.11.1",
|
||||
"sse.js": "^0.6.1",
|
||||
"ui": "*",
|
||||
|
||||
@@ -72,76 +72,87 @@ When the user provides consent, Google issues an identity token (commonly abbrev
|
||||
If you are building a Flutter app, you can use [flutter_appauth](https://pub.dev/packages/flutter_appauth) package to sign a user into your Supabase project:
|
||||
|
||||
````dart
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter_appauth/flutter_appauth.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
// Generate a random string
|
||||
final rawNonce = generateRandomString();
|
||||
final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();
|
||||
|
||||
// Your registered Google client ID here.
|
||||
// This will be different for iOS and Android
|
||||
const clientId = 'YOUR_GOODLE_CLIENT_ID';
|
||||
|
||||
// bundle ID for iOS, package name for Android here
|
||||
final packageName = 'io.supabase.example';
|
||||
|
||||
/// fixed for google login
|
||||
final redirectUrl = '$packageName:/google_auth';
|
||||
|
||||
/// fixed for google login
|
||||
const discoveryUrl =
|
||||
'https://accounts.google.com/.well-known/openid-configuration';
|
||||
|
||||
final appAuth = FlutterAppAuth();
|
||||
|
||||
// authorize the user by opening the consent page
|
||||
final result = await appAuth.authorize(
|
||||
AuthorizationRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl: discoveryUrl,
|
||||
nonce: hashedNonce,
|
||||
scopes: [
|
||||
'openid',
|
||||
'email',
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
throw AuthException(
|
||||
'Could not find AuthorizationResponse after authorizing');
|
||||
/// Function to generate a random 16 character string.
|
||||
String _generateRandomString() {
|
||||
final random = Random.secure();
|
||||
return base64Url.encode(List<int>.generate(16, (_) => random.nextInt(256)));
|
||||
}
|
||||
|
||||
// Request the access and id token to google
|
||||
final tokenResponse = await appAuth.token(
|
||||
TokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
authorizationCode: result.authorizationCode,
|
||||
discoveryUrl: discoveryUrl,
|
||||
codeVerifier: result.codeVerifier,
|
||||
nonce: result.nonce,
|
||||
scopes: [
|
||||
'openid',
|
||||
'email',
|
||||
],
|
||||
),
|
||||
);
|
||||
Future<AuthResponse> signInWithGoogle() {
|
||||
// Just a random string
|
||||
final rawNonce = _generateRandomString();
|
||||
final hashedNonce =
|
||||
sha256.convert(utf8.encode(rawNonce)).toString();
|
||||
|
||||
final idToken = tokenResponse?.idToken;
|
||||
/// TODO: update the client ID with your own
|
||||
///
|
||||
/// Client ID that you registered with Google Cloud.
|
||||
/// You will have two different values for iOS and Android.
|
||||
const clientId = 'YOUR_CLIENT_ID_HERE';
|
||||
|
||||
if (idToken == null) {
|
||||
throw AuthException('Could not find idToken from the token response');
|
||||
/// reverse DNS form of the client ID + `:/` is set as the redirect URL
|
||||
final redirectUrl = '${clientId.split('.').reversed.join('.')}:/';
|
||||
|
||||
/// Fixed value for google login
|
||||
const discoveryUrl =
|
||||
'https://accounts.google.com/.well-known/openid-configuration';
|
||||
|
||||
final appAuth = FlutterAppAuth();
|
||||
|
||||
// authorize the user by opening the consent page
|
||||
final result = await appAuth.authorize(
|
||||
AuthorizationRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl: discoveryUrl,
|
||||
nonce: hashedNonce,
|
||||
scopes: [
|
||||
'openid',
|
||||
'email',
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
throw 'Could not find AuthorizationResponse after authorizing';
|
||||
}
|
||||
|
||||
// Request the access and id token to google
|
||||
final tokenResult = await appAuth.token(
|
||||
TokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
authorizationCode: result.authorizationCode,
|
||||
discoveryUrl: discoveryUrl,
|
||||
codeVerifier: result.codeVerifier,
|
||||
nonce: result.nonce,
|
||||
scopes: [
|
||||
'openid',
|
||||
'email',
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final idToken = tokenResult?.idToken;
|
||||
|
||||
if (idToken == null) {
|
||||
throw 'Could not find idToken from the token response';
|
||||
}
|
||||
|
||||
return supabase.auth.signInWithIdToken(
|
||||
provider: Provider.google,
|
||||
idToken: idToken,
|
||||
accessToken: tokenResponse?.accessToken,
|
||||
nonce: rawNonce,
|
||||
);
|
||||
}
|
||||
|
||||
await signInWithIdToken(
|
||||
provider: Provider.google,
|
||||
idToken: idToken,
|
||||
accessToken: tokenResponse?.accessToken,
|
||||
nonce: rawNonce,
|
||||
);```
|
||||
```
|
||||
|
||||
In the Supabase JavaScript library, which you can use with web-based native frameworks like React Native or Expo, you can invoke this functionality like so:
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
@@ -31,6 +32,13 @@ const pageMap = [
|
||||
},
|
||||
remoteFile: 'generating-types.md',
|
||||
},
|
||||
{
|
||||
slug: 'api',
|
||||
meta: {
|
||||
title: 'GraphQL API',
|
||||
},
|
||||
remoteFile: 'api.md',
|
||||
},
|
||||
]
|
||||
|
||||
interface ActionDocsProps {
|
||||
@@ -116,6 +124,7 @@ export const getStaticProps: GetStaticProps<ActionDocsProps> = async ({ params }
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
remarkMkDocsAdmonition,
|
||||
remarkPyMdownTabs,
|
||||
[removeTitle, meta.title],
|
||||
[remarkCodeHike, codeHikeOptions],
|
||||
],
|
||||
|
||||
@@ -62,7 +62,7 @@ insert into "Blog"(name)
|
||||
values ('My Blog');
|
||||
```
|
||||
|
||||
The reflected GraphQL schema can be queries immediately as
|
||||
The reflected GraphQL schema can be queried immediately as
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
|
||||
import { GetStaticPaths, GetStaticProps } from 'next'
|
||||
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import { relative } from 'path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import emoji from 'remark-emoji'
|
||||
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
|
||||
import components from '~/components'
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
const repo = 'wrappers'
|
||||
const branch = 'main'
|
||||
const docsDir = 'docs'
|
||||
const externalSite = 'https://supabase.github.io/wrappers'
|
||||
|
||||
// Each external docs page is mapped to a local page
|
||||
const pageMap = [
|
||||
{
|
||||
slug: 'airtable',
|
||||
meta: {
|
||||
title: 'Airtable',
|
||||
},
|
||||
remoteFile: 'airtable.md',
|
||||
},
|
||||
{
|
||||
slug: 's3',
|
||||
meta: {
|
||||
title: 'AWS S3',
|
||||
},
|
||||
remoteFile: 's3.md',
|
||||
},
|
||||
{
|
||||
slug: 'bigquery',
|
||||
meta: {
|
||||
title: 'BigQuery',
|
||||
},
|
||||
remoteFile: 'bigquery.md',
|
||||
},
|
||||
{
|
||||
slug: 'clickhouse',
|
||||
meta: {
|
||||
title: 'ClickHouse',
|
||||
},
|
||||
remoteFile: 'clickhouse.md',
|
||||
},
|
||||
{
|
||||
slug: 'firebase',
|
||||
meta: {
|
||||
title: 'Firebase',
|
||||
},
|
||||
remoteFile: 'firebase.md',
|
||||
},
|
||||
{
|
||||
slug: 'logflare',
|
||||
meta: {
|
||||
title: 'Logflare',
|
||||
},
|
||||
remoteFile: 'logflare.md',
|
||||
},
|
||||
{
|
||||
slug: 'stripe',
|
||||
meta: {
|
||||
title: 'Stripe',
|
||||
},
|
||||
remoteFile: 'stripe.md',
|
||||
},
|
||||
]
|
||||
|
||||
interface WrappersDocsProps {
|
||||
source: MDXRemoteSerializeResult
|
||||
meta: {
|
||||
title: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function WrappersDocs({ source, meta }: WrappersDocsProps) {
|
||||
return (
|
||||
<Layout meta={meta}>
|
||||
<MDXRemote {...source} components={components} />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch markdown from external repo and transform links
|
||||
*/
|
||||
export const getStaticProps: GetStaticProps<WrappersDocsProps> = async ({ params }) => {
|
||||
const page = pageMap.find(({ slug }) => slug === params.slug)
|
||||
|
||||
if (!page) {
|
||||
throw new Error(`No page mapping found for slug '${params.slug}'`)
|
||||
}
|
||||
|
||||
const { remoteFile, meta } = page
|
||||
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
|
||||
)
|
||||
|
||||
const source = await response.text()
|
||||
|
||||
const urlTransform: UrlTransformFunction = (url) => {
|
||||
try {
|
||||
const externalSiteUrl = new URL(externalSite)
|
||||
|
||||
const placeholderHostname = 'placeholder'
|
||||
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
|
||||
|
||||
// Don't modify a url with a FQDN or a url that's only a hash
|
||||
if (hostname !== placeholderHostname || pathname === '/') {
|
||||
return url
|
||||
}
|
||||
|
||||
const relativePage = (
|
||||
pathname.endsWith('.md')
|
||||
? pathname.replace(/\.md$/, '')
|
||||
: relative(externalSiteUrl.pathname, pathname)
|
||||
).replace(/^\//, '')
|
||||
|
||||
const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile)
|
||||
|
||||
// If we have a mapping for this page, use the mapped path
|
||||
if (page) {
|
||||
return page.slug + hash
|
||||
}
|
||||
|
||||
// If we don't have this page in our docs, link to original docs
|
||||
return `${externalSite}/${relativePage}${hash}`
|
||||
} catch (err) {
|
||||
console.error('Error transforming markdown URL', err)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const codeHikeOptions: CodeHikeConfig = {
|
||||
theme: codeHikeTheme,
|
||||
lineNumbers: true,
|
||||
showCopyButton: true,
|
||||
skipLanguages: [],
|
||||
autoImport: false,
|
||||
}
|
||||
|
||||
const mdxSource = await serialize(source, {
|
||||
scope: {
|
||||
chCodeConfig: codeHikeOptions,
|
||||
},
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
remarkMkDocsAdmonition,
|
||||
emoji,
|
||||
remarkPyMdownTabs,
|
||||
[removeTitle, meta.title],
|
||||
[remarkCodeHike, codeHikeOptions],
|
||||
],
|
||||
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
|
||||
},
|
||||
})
|
||||
|
||||
return { props: { source: mdxSource, meta } }
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
return {
|
||||
paths: pageMap.map(({ slug }) => ({
|
||||
params: {
|
||||
slug,
|
||||
},
|
||||
})),
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'wrappers',
|
||||
title: 'wrappers: 3rd Party Integrations',
|
||||
title: 'Wrappers: 3rd Party Integrations',
|
||||
description: '3rd party integrations for PostgreSQL ',
|
||||
}
|
||||
|
||||
@@ -60,12 +60,13 @@ A web interface for connecting to external data is coming to Supabase Studio in
|
||||
|
||||
## Integrations
|
||||
|
||||
- [Firebase](https://supabase.github.io/wrappers/firebase/)
|
||||
- [Stripe](https://supabase.github.io/wrappers/stripe/)
|
||||
- [BigQuery](https://supabase.github.io/wrappers/bigquery/)
|
||||
- [AWS S3](https://supabase.github.io/wrappers/s3/)
|
||||
- Airtable - Coming Soon
|
||||
- Clickhouse - Coming Soon
|
||||
- [Airtable](/docs/guides/database/extensions/wrappers/airtable)
|
||||
- [AWS S3](/docs/guides/database/extensions/wrappers/s3)
|
||||
- [BigQuery](/docs/guides/database/extensions/wrappers/bigquery)
|
||||
- [ClickHouse](/docs/guides/database/extensions/wrappers/clickhouse)
|
||||
- [Firebase](/docs/guides/database/extensions/wrappers/firebase)
|
||||
- [Logflare](/docs/guides/database/extensions/wrappers/logflare)
|
||||
- [Stripe](/docs/guides/database/extensions/wrappers/stripe)
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -73,3 +73,15 @@ max_parallel_workers |3 |
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
|
||||
## Pooler Config
|
||||
|
||||
You can also [customize some parameters](https://supabase.com/dashboard/project/_/settings/database) for the Connection Pooler:
|
||||
|
||||
1. [Pooling Mode](https://devcenter.heroku.com/articles/best-practices-pgbouncer-configuration#pgbouncer-s-connection-pooling-modes)
|
||||
1. [Default Pool Size](https://www.pgbouncer.org/config.html)
|
||||
1. [Max Client Connections](https://www.pgbouncer.org/config.html)
|
||||
|
||||
The default pool size, and the maximum number of clients allowed to connect concurrently is automatically optimized based on the compute add-on being used. At the moment, the Dashboard only reflects any custom configuration being used, and does not include the default optimized numbers used for your project.
|
||||
|
||||
Custom Pooler Config may also require manual updates to any relevant overrides when changing compute add-ons.
|
||||
@@ -48,7 +48,7 @@ We can use your JWT Secret to generate new `anon` and `service` API keys using t
|
||||
|
||||
### Update API Keys
|
||||
|
||||
Replace the values the `.env` file:
|
||||
Replace the values in the `.env` file:
|
||||
|
||||
- `ANON_KEY` - replace with an `anon` key
|
||||
- `SERVICE_ROLE_KEY` - replace with a `service` key
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: 'Supabase Launch Week 8 Hackathon'
|
||||
description: Build an Open Source Project over 10 days. 5 prize categories.
|
||||
author: ant_wilson
|
||||
image: lw8-hackathon/thumbnail.png
|
||||
thumb: lw8-hackathon/thumbnail.png
|
||||
launchweek: 8
|
||||
tags:
|
||||
- launch-week
|
||||
- hackathon
|
||||
date: '2023-07-25'
|
||||
toc_depth: 2
|
||||
---
|
||||
|
||||
[Supabase Launch Week 8](https://supabase.com/launch-week) is about to start and we're running an async hackathon alongside it.
|
||||
|
||||
The hackathon starts Friday 4th August at 09:00 am PT and ends Sunday 13th August at 11:59 pm PT. You can win extremely limited edition Launch Week swag and add your name to the Supabase Hackathon Hall of Fame.
|
||||
|
||||
For some inspiration check out all the [winners from previous hackathons](https://supabase.com/blog/tags/hackathon).
|
||||
|
||||
This is the perfect excuse to "Build in a weekend, scale to millions". Since you retain all the rights to your submissions, you can use the hackathon as a launch pad for your new Startup ideas, side-project, or indie hack.
|
||||
|
||||

|
||||
|
||||
## Key Facts
|
||||
|
||||
- You have 10 days to build a new **Open Source** project using Supabase in some capacity.
|
||||
- Starting 09:00 am PT Friday 4th August 2023
|
||||
- The submission deadline is 11:59 pm Sunday midnight PT 14th August 2023
|
||||
- Enter as an individual, or as a team of up to 5 people
|
||||
- Build whatever you want - a project, app, tool, library. Anything.
|
||||
|
||||
## Prizes
|
||||
|
||||
There are 5 categories, there will be prizes for:
|
||||
|
||||
- Best overall project
|
||||
- Best use of AI
|
||||
- Most fun / best easter egg
|
||||
- Most technically impressive
|
||||
- Most visually pleasing
|
||||
|
||||
There will be a winner and a runner-up prize for each category. Every team member on winning/runner-up teams gets a Supabase Launch Week swag kit.
|
||||
|
||||

|
||||
|
||||
## Submission
|
||||
|
||||
You should submit your project using [this form](https://madewithsupabase.com/hackathons/launch-week-8) before 11:59 pm Sunday midnight PT 14th August 2023. Extra points if you include a simple video demoing the app in the description.
|
||||
|
||||
## Judges
|
||||
|
||||
The Supabase team will judge the winners for each category.
|
||||
We will be looking for:
|
||||
|
||||
- creativity/inventiveness
|
||||
- functions correctly/smoothly
|
||||
- visually pleasing
|
||||
- technically impressive
|
||||
- use of Supabase features
|
||||
- FUN! 😃
|
||||
|
||||
## Rules
|
||||
|
||||
- Team size 1-5 (all team members on winning teams will receive a prize)
|
||||
- You cannot be on multiple teams
|
||||
- One submission per team
|
||||
- It's not a requirement to use AI
|
||||
- All design elements, code, etc. for your project must be created **during** the event
|
||||
- All entries must be Open Source (link to source code required in entry)
|
||||
- Must use Supabase in some capacity
|
||||
- Can be any language or framework
|
||||
- You must submit before the deadline (no late entries)
|
||||
- (optional) Include a link to a demo video along with the submission for extra points
|
||||
|
||||
## Resources
|
||||
|
||||
### Supabase resources to get you started
|
||||
|
||||
- [OpenAI with Edge Functions YouTube video](https://www.youtube.com/watch?v=29p8kIqyU_Y)
|
||||
- [Streaming Data in Edge Functions](https://www.youtube.com/watch?v=9N66JBRLNYU)
|
||||
- [Storing OpenAI embeddings in Postgres with pgvector article](https://supabase.com/blog/openai-embeddings-postgres-vector)
|
||||
- [pgvector guide](https://supabase.com/docs/guides/database/extensions/pgvector)
|
||||
|
||||
### Community help
|
||||
|
||||
The Supabase Team will be taking part in the Hackathon and you'll find us live to build in our discord all week. Please join us by building in public:
|
||||
|
||||
- Text channel: [#hackathon](https://discord.gg/UYyweApy)
|
||||
- Audio channel: [#hackathon](https://discord.gg/Vj3mTPwH)
|
||||
|
||||
If you need help or advice when building, find other people to join your team, or if you just want to chill and watch people build, come and join us!
|
||||
|
||||
[Join our Discord](https://discord.supabase.com/)
|
||||
|
||||

|
||||
|
||||
## Launch Week
|
||||
|
||||
Don't forget to check out the new features being announced as part of [Launch Week](https://supabase.com/launch-week).
|
||||
|
||||
- [Previous Hackathon Prize Winners](https://supabase.com/blog/tags/hackathon)
|
||||
|
||||
## Additional Info
|
||||
|
||||
- Any intellectual property developed during the hackathon will belong to the team that developed it. We expect that each team will have an agreement between themselves regarding the IP, but this is not required.
|
||||
- By making a submission you grant Supabase permission to use screenshots, code snippets, and/or links to your project or content of your README on our Twitter, blog, website, email updates, and in the Supabase discord server. Supabase does not make any claims over your IP.
|
||||
@@ -0,0 +1,625 @@
|
||||
---
|
||||
title: 'React Native file upload with Supabase Storage'
|
||||
description: Learn how to implement authentication and file upload in a React Native app.
|
||||
author: simon_grimm
|
||||
image: 2023-08-01-react-native-storage/react-native-storage.png
|
||||
thumb: 2023-08-01-react-native-storage/react-native-storage.png
|
||||
tags:
|
||||
- react-native
|
||||
- storage
|
||||
date: '2023-08-01'
|
||||
toc_depth: 2
|
||||
---
|
||||
|
||||
If you want to upload files from your React Native app, you need a backend to store the files, [Supabase Storage](https://supabase.com/docs/guides/storage) is a great choice for this as it provides a simple API to upload files, and we can easily combine this with authentication to build a powerful app.
|
||||
|
||||
This means you can quickly build your own image-sharing app, a file-sharing app, or any other app that needs to upload files to a backend!
|
||||
|
||||

|
||||
|
||||
In this tutorial you will learn to:
|
||||
|
||||
- Set up a React Native app with [Expo SDK49](https://expo.dev/)
|
||||
- Use [Supabase Authentication](https://supabase.com/docs/guides/auth)
|
||||
- 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!
|
||||
|
||||
Before we get into the app, let's quickly [start a new Supabase project](https://supabase.com/dashboard).
|
||||
|
||||
## Creating the Supabase Project
|
||||
|
||||
To use authentication and storage we need a new Supabase project. If you don't have a Supabase account yet, you can [get started for free](https://supabase.com/dashboard)!
|
||||
|
||||
In your dashboard, click "New Project" and leave it to the default settings, but make sure you keep a copy o your Database password!
|
||||
|
||||

|
||||
|
||||
After a minute your project should be ready, and we can configure the authentication and storage.
|
||||
|
||||
## Setting up Authentication
|
||||
|
||||
Authentication will be enabled by default, but we want to turn off email confirmation for this tutorial.
|
||||
|
||||
Select **Authentication** from the menu, go to the **Providers** section, and expand Email.
|
||||
|
||||

|
||||
|
||||
Here you can disable the confirmation email, and apply other changes later if you want to!
|
||||
|
||||
## Setting up Storage
|
||||
|
||||
Now we want to create a **bucket** under storage where we will upload our files, and also add some security rules to protect the files of a user.
|
||||
|
||||
First, select **Storage** from the menu, then click **New bucket** and call it `files`.
|
||||
|
||||

|
||||
|
||||
Make sure that this is not a public bucket, otherwise, even unauthenticated users can upload or read the files!
|
||||
|
||||
To protect that bucket and allow users only access to their own folder, we need to add some [Storage policies](https://supabase.com/docs/guides/storage/access-control).
|
||||
|
||||
You can either do this through the UI and pick from examples, or simply run my SQL script in the **SQL Editor** which you can select from the menu:
|
||||
|
||||
```sql
|
||||
CREATE POLICY "Enable storage access for users based on user_id" ON "storage"."objects"
|
||||
AS PERMISSIVE FOR ALL
|
||||
TO public
|
||||
USING (bucket_id = 'files' AND auth.uid()::text = (storage.foldername(name))[1])
|
||||
WITH CHECK (bucket_id = 'files' AND auth.uid()::text = (storage.foldername(name))[1])
|
||||
```
|
||||
|
||||
This will allow users to only access their own folder, and not any other files in the bucket.
|
||||
|
||||
## Setting up the React Native app
|
||||
|
||||
Now that we have our Supabase project ready, we can start building the React Native app!
|
||||
|
||||
Get started by setting up a new Expo app with the tabs template and install some dependencies:
|
||||
|
||||
```sh
|
||||
# Create a new Expo app
|
||||
npx create-expo-app@latest cloudApp --template tabs@49
|
||||
|
||||
# Install dependencies
|
||||
npm i @supabase/supabase-js
|
||||
npm i react-native-url-polyfill base64-arraybuffer react-native-loading-spinner-overlay
|
||||
|
||||
# Install Expo packages
|
||||
npx expo install expo-secure-store
|
||||
npx expo install expo-image-picker
|
||||
npx expo install expo-file-system
|
||||
```
|
||||
|
||||
We will use the [Expo Secure Store](https://docs.expo.dev/versions/latest/sdk/securestore/) to store the Supabase session, and the [Expo Image Picker](https://docs.expo.dev/versions/latest/sdk/imagepicker/) to select images from the device. We also need the [Expo File System](https://docs.expo.dev/versions/latest/sdk/filesystem/) to read the image from the device and upload its data.
|
||||
|
||||
You can now already run your project with `npx expo` and then select a platform to run on.
|
||||
|
||||
However, the tabs template contains a lot of code that we don't need, so to simplify things we can remove the **app**, **constants** and **components** folder.
|
||||
|
||||
This gives us a much cleaner project structure.
|
||||
|
||||
## Connecting to Supabase from React Native
|
||||
|
||||
To use Supabase we need to initialize the client with our project URL and the public key, which you can find in the **Settings** of your project under **API**.
|
||||
|
||||
You can put both of them in a `.env` file at the root of your project:
|
||||
|
||||
```env
|
||||
EXPO_PUBLIC_SUPABASE_URL=
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=
|
||||
```
|
||||
|
||||
We can now simply read those values from the environment variables and initialize the Supabase client, so create a file at `config/initSupabase.ts` and add the following code:
|
||||
|
||||
```ts
|
||||
import * as SecureStore from 'expo-secure-store'
|
||||
import 'react-native-url-polyfill/auto'
|
||||
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// Use a custom secure storage solution for the Supabase client to store the JWT
|
||||
const ExpoSecureStoreAdapter = {
|
||||
getItem: (key: string) => {
|
||||
return SecureStore.getItemAsync(key)
|
||||
},
|
||||
setItem: (key: string, value: string) => {
|
||||
SecureStore.setItemAsync(key, value)
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
SecureStore.deleteItemAsync(key)
|
||||
},
|
||||
}
|
||||
|
||||
const url = process.env.EXPO_PUBLIC_SUPABASE_URL
|
||||
const key = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY
|
||||
|
||||
// Initialize the Supabase client
|
||||
export const supabase = createClient(url, key, {
|
||||
auth: {
|
||||
storage: ExpoSecureStoreAdapter as any,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
We are using the SecureStore from Expo to handle the session of our Supabase client and add in the `createClient` function.
|
||||
|
||||
Later we can import the `supabase` client from this file and use it in our app whenever we need to access Supabase.
|
||||
|
||||
## Building the authentication flow
|
||||
|
||||
Currently, the app won't work as we have no entry point. Because we are using the Expon Router and file-based routing, we can create a new file at `app/index.tsx` which will be the first page that comes up in our app.
|
||||
|
||||
On this page we will handle both login and registration, so let's start by creating a simple form with a few inputs and buttons inside the `app/index.tsx` file:
|
||||
|
||||
```tsx
|
||||
import { Alert, View, Button, TextInput, StyleSheet, Text, TouchableOpacity } from 'react-native'
|
||||
import { useState } from 'react'
|
||||
import React from 'react'
|
||||
import Spinner from 'react-native-loading-spinner-overlay'
|
||||
import { supabase } from '../config/initSupabase'
|
||||
|
||||
const Login = () => {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Sign in with email and password
|
||||
const onSignInPress = async () => {
|
||||
setLoading(true)
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
if (error) Alert.alert(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
const onSignUpPress = async () => {
|
||||
setLoading(true)
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
|
||||
if (error) Alert.alert(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Spinner visible={loading} />
|
||||
|
||||
<Text style={styles.header}>My Cloud</Text>
|
||||
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
placeholder="john@doe.com"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
style={styles.inputField}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
style={styles.inputField}
|
||||
/>
|
||||
|
||||
<TouchableOpacity onPress={onSignInPress} style={styles.button}>
|
||||
<Text style={{ color: '#fff' }}>Sign in</Text>
|
||||
</TouchableOpacity>
|
||||
<Button onPress={onSignUpPress} title="Create Account" color={'#fff'}></Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 200,
|
||||
padding: 20,
|
||||
backgroundColor: '#151515',
|
||||
},
|
||||
header: {
|
||||
fontSize: 30,
|
||||
textAlign: 'center',
|
||||
margin: 50,
|
||||
color: '#fff',
|
||||
},
|
||||
inputField: {
|
||||
marginVertical: 4,
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderColor: '#2b825b',
|
||||
borderRadius: 4,
|
||||
padding: 10,
|
||||
color: '#fff',
|
||||
backgroundColor: '#363636',
|
||||
},
|
||||
button: {
|
||||
marginVertical: 15,
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#2b825b',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
},
|
||||
})
|
||||
|
||||
export default Login
|
||||
```
|
||||
|
||||
There's nothing fancy going on here, but this is all we need to use Supabase Authentication in our app!
|
||||
|
||||

|
||||
|
||||
You can try it out right now and create a new user account or sign in with an existing one and log the values to the console to see what's going on.
|
||||
|
||||
However, we are not handling the authentication state yet so let's create a **Context** to listen to changes.
|
||||
|
||||
We will wrap a Provider around our app, which will use the `onAuthStateChange` function from Supabase to listen to changes in the authentication state and accordingly set our state.
|
||||
|
||||
For this, create a new file at `provider/AuthProvider.tsx` and add the following code:
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect, createContext, PropsWithChildren } from 'react'
|
||||
import { Session, User } from '@supabase/supabase-js'
|
||||
import { supabase } from '../config/initSupabase'
|
||||
|
||||
type AuthProps = {
|
||||
user: User | null
|
||||
session: Session | null
|
||||
initialized?: boolean
|
||||
signOut?: () => void
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<Partial<AuthProps>>({})
|
||||
|
||||
// Custom hook to read the context values
|
||||
export function useAuth() {
|
||||
return React.useContext(AuthContext)
|
||||
}
|
||||
|
||||
export const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
const [user, setUser] = useState<User | null>()
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [initialized, setInitialized] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for changes to authentication state
|
||||
const { data } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
setSession(session)
|
||||
setUser(session ? session.user : null)
|
||||
setInitialized(true)
|
||||
})
|
||||
return () => {
|
||||
data.subscription.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Log out the user
|
||||
const signOut = async () => {
|
||||
await supabase.auth.signOut()
|
||||
}
|
||||
|
||||
const value = {
|
||||
user,
|
||||
session,
|
||||
initialized,
|
||||
signOut,
|
||||
}
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
```
|
||||
|
||||
To use the context we can now wrap it around our app, and while we do this we can also take care of the navigation:
|
||||
|
||||
In the topmost layout file we can check whether a user has an active session or not, and either directly sign the user into the inside area (that we will create soon) or automatically bring her back to the login screen.
|
||||
|
||||
To make this work with the Expo Router we can create a file at `app/_layout.tsx` and add the following code:
|
||||
|
||||
```tsx
|
||||
import { Slot, useRouter, useSegments } from 'expo-router'
|
||||
import { useEffect } from 'react'
|
||||
import { AuthProvider, useAuth } from '../provider/AuthProvider'
|
||||
|
||||
// Makes sure the user is authenticated before accessing protected pages
|
||||
const InitialLayout = () => {
|
||||
const { session, initialized } = useAuth()
|
||||
const segments = useSegments()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) return
|
||||
|
||||
// Check if the path/url is in the (auth) group
|
||||
const inAuthGroup = segments[0] === '(auth)'
|
||||
|
||||
if (session && !inAuthGroup) {
|
||||
// Redirect authenticated users to the list page
|
||||
router.replace('/list')
|
||||
} else if (!session) {
|
||||
// Redirect unauthenticated users to the login page
|
||||
router.replace('/')
|
||||
}
|
||||
}, [session, initialized])
|
||||
|
||||
return <Slot />
|
||||
}
|
||||
|
||||
// Wrap the app with the AuthProvider
|
||||
const RootLayout = () => {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<InitialLayout />
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default RootLayout
|
||||
```
|
||||
|
||||
Whenever the `initialized` or `session` state changes, we check if the user is authenticated and redirect her to the correct page.
|
||||
|
||||
This also means we don't have to worry about the authentication state in our pages anymore, we can just assume that the user is authenticated and use the `useAuth` hook to access the user and session data later on.
|
||||
|
||||
Your app might show an error right now because the `/list` route doesn't exist yet, but we will create it in the next step.
|
||||
|
||||
## File Upload to Supabase Storage
|
||||
|
||||
Now that we have the authentication set up, we can start working on the file upload.
|
||||
|
||||
First, let's define another layout for this inside area so create a file at `/app/(auth)/_layout.tsx` and add the following code:
|
||||
|
||||
```tsx
|
||||
import { Stack } from 'expo-router'
|
||||
import { useAuth } from '../../provider/AuthProvider'
|
||||
import React from 'react'
|
||||
import { TouchableOpacity } from 'react-native'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
|
||||
// Simple stack layout within the authenticated area
|
||||
const StackLayout = () => {
|
||||
const { signOut } = useAuth()
|
||||
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: {
|
||||
backgroundColor: '#0f0f0f',
|
||||
},
|
||||
headerTintColor: '#fff',
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="list"
|
||||
options={{
|
||||
headerTitle: 'My Files',
|
||||
headerRight: () => (
|
||||
<TouchableOpacity onPress={signOut}>
|
||||
<Ionicons name="log-out-outline" size={30} color={'#fff'} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
}}
|
||||
></Stack.Screen>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default StackLayout
|
||||
```
|
||||
|
||||
This defines a simple stack navigation and adds a button to trigger the logout, so we can now also fully test the authentication flow.
|
||||
|
||||
Next, we create the page for uploading and displaying all files of a user from Supabase Storage.
|
||||
|
||||
You won't have any files to show yet, but loading the files of a user is as easy as calling `list()` on the storage bucket and passing the user id as the folder name.
|
||||
|
||||
Additionally, we add a little FAB (floating action button) to trigger the file picker, so create a file at `/app/(auth)/list.tsx` and add the following code:
|
||||
|
||||
```tsx
|
||||
import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
import { useAuth } from '../../provider/AuthProvider'
|
||||
import * as FileSystem from 'expo-file-system'
|
||||
import { decode } from 'base64-arraybuffer'
|
||||
import { supabase } from '../../config/initSupabase'
|
||||
import { FileObject } from '@supabase/storage-js'
|
||||
|
||||
const list = () => {
|
||||
const { user } = useAuth()
|
||||
const [files, setFiles] = useState<FileObject[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return
|
||||
|
||||
// Load user images
|
||||
loadImages()
|
||||
}, [user])
|
||||
|
||||
const loadImages = async () => {
|
||||
const { data } = await supabase.storage.from('files').list(user!.id)
|
||||
if (data) {
|
||||
setFiles(data)
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectImage = async () => {
|
||||
// TODO
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* FAB to add images */}
|
||||
<TouchableOpacity onPress={onSelectImage} style={styles.fab}>
|
||||
<Ionicons name="camera-outline" size={30} color={'#fff'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
backgroundColor: '#151515',
|
||||
},
|
||||
fab: {
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 70,
|
||||
position: 'absolute',
|
||||
bottom: 40,
|
||||
right: 30,
|
||||
height: 70,
|
||||
backgroundColor: '#2b825b',
|
||||
borderRadius: 100,
|
||||
},
|
||||
})
|
||||
|
||||
export default list
|
||||
```
|
||||
|
||||
This should give us a nice and clean UI.
|
||||
|
||||

|
||||
|
||||
Now we can implement the image picker and upload the selected image to Supabase Storage.
|
||||
|
||||
Using the image picker from Expo gives us a **URI**, which we can use to read the file from the file system and convert it to a base64 string.
|
||||
|
||||
We can then use the `upload()` method from the storage client to upload the file to the storage bucket. Life can be easy.
|
||||
|
||||
At this point, you should be able to upload files to Supabase Storage, and you can already see them in your UI (or log them to the console).
|
||||
|
||||
To finally display them we will add a `ScrollView` component, which will render one item for every file of a user.
|
||||
|
||||
Let's start by creating those component rows in another file, so create a `components/ImageItem.tsx` file and add the following code:
|
||||
|
||||
```tsx
|
||||
import { FileObject } from '@supabase/storage-js'
|
||||
import { Image, View, Text, TouchableOpacity } from 'react-native'
|
||||
import { supabase } from '../config/initSupabase'
|
||||
import { useState } from 'react'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
|
||||
// Image item component that displays the image from Supabase Storage and a delte button
|
||||
const ImageItem = ({
|
||||
item,
|
||||
userId,
|
||||
onRemoveImage,
|
||||
}: {
|
||||
item: FileObject
|
||||
userId: string
|
||||
onRemoveImage: () => void
|
||||
}) => {
|
||||
const [image, setImage] = useState<string>('')
|
||||
|
||||
supabase.storage
|
||||
.from('files')
|
||||
.download(`${userId}/${item.name}`)
|
||||
.then(({ data }) => {
|
||||
const fr = new FileReader()
|
||||
fr.readAsDataURL(data!)
|
||||
fr.onload = () => {
|
||||
setImage(fr.result as string)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', margin: 1, alignItems: 'center', gap: 5 }}>
|
||||
{image ? (
|
||||
<Image style={{ width: 80, height: 80 }} source={{ uri: image }} />
|
||||
) : (
|
||||
<View style={{ width: 80, height: 80, backgroundColor: '#1A1A1A' }} />
|
||||
)}
|
||||
<Text style={{ flex: 1, color: '#fff' }}>{item.name}</Text>
|
||||
{/* Delete image button */}
|
||||
<TouchableOpacity onPress={onRemoveImage}>
|
||||
<Ionicons name="trash-outline" size={20} color={'#fff'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageItem
|
||||
```
|
||||
|
||||
This component will display the image from Supabase Storage, the name of the file and a delete button.
|
||||
|
||||
To display the image we use the `download()` method from the storage client, which returns a `FileObject` with the file data. We can then use the `FileReader` to convert the file data to a base64 string, which we can use as the image source.
|
||||
|
||||
Now let's use this component in our `list.tsx` file to render the list of images by updating the `return` statement:
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView>
|
||||
{files.map((item, index) => (
|
||||
<ImageItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
userId={user!.id}
|
||||
onRemoveImage={() => onRemoveImage(item, index)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* FAB to add images */}
|
||||
<TouchableOpacity onPress={onSelectImage} style={styles.fab}>
|
||||
<Ionicons name="camera-outline" size={30} color={'#fff'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
```
|
||||
|
||||
Don't forget to also include the import to the `ImageItem` component!
|
||||
|
||||

|
||||
|
||||
Finally, we can also include the delete functionality by adding the following code to the `list.tsx`:
|
||||
|
||||
```tsx
|
||||
const onRemoveImage = async (item: FileObject, listIndex: number) => {
|
||||
supabase.storage.from('files').remove([`${user!.id}/${item.name}`])
|
||||
const newFiles = [...files]
|
||||
newFiles.splice(listIndex, 1)
|
||||
setFiles(newFiles)
|
||||
}
|
||||
```
|
||||
|
||||
We are handling the deletion here so we can accordingly update the state of the files list after removing an item.
|
||||
|
||||
And with that in place, you have a fully functional image gallery app with React Native and Supabase Storage!
|
||||
|
||||
## What about resumable uploads?
|
||||
|
||||
Initially, I wanted to include [resumable uploads](https://supabase.com/blog/storage-v3-resumable-uploads) in this tutorial, but apparently, the [Uppy](https://uppy.io/) client didn't work 100% for React Native yet.
|
||||
|
||||
You can still see an [initial implementation of resumable downloads with Supabase and React Native](https://github.com/saimon24/react-native-resumable-upload-supabase/commit/bb45455dc2d4d3fdd42089a8a26a521cc5b5e60f#diff-2d97fcb7ad2b1ac426a8d5391cfd0ab970e525ede1f11e14885f65e6dcd116a1) in the repository of this tutorial.
|
||||
|
||||
However, ultimately the uploaded file was always 0 bytes, so I decided to leave it out for now.
|
||||
|
||||
The Supabas team is investigating this issue, so I'm very sure that we will have resumable uploads working with React Native soon.
|
||||
|
||||
## Conclusion
|
||||
|
||||
It's almost too easy to use Supabase Storage, and it's a great way to store files for your apps.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Until next time and happy coding with Supabase!
|
||||
@@ -11,27 +11,31 @@ type Props = {
|
||||
description?: string
|
||||
url?: string
|
||||
icon?: string
|
||||
svg?: any
|
||||
}
|
||||
|
||||
const Developers = () => {
|
||||
const { basePath } = useRouter()
|
||||
|
||||
const iconSections = links.map((link: Props) => {
|
||||
const { text, description, url, icon } = link
|
||||
const { text, description, url, icon, svg: Svg } = link
|
||||
|
||||
const content = (
|
||||
<div className="dark:hover:bg-scale-500 -m-3 flex items-start rounded-lg p-3 transition duration-150 ease-in-out hover:bg-gray-50">
|
||||
{/* <!-- Heroicon name: support --> */}
|
||||
<svg
|
||||
className="stroke-scale-900 h-5 w-5 flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d={icon} />
|
||||
</svg>
|
||||
{icon && (
|
||||
<svg
|
||||
className="stroke-scale-900 h-5 w-5 flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d={icon} />
|
||||
</svg>
|
||||
)}
|
||||
{Svg && <Svg />}
|
||||
<div className="ml-4">
|
||||
<h5 className="text-scale-1200 text-base">{text}</h5>
|
||||
<p className="text-scale-900 text-sm">{description}</p>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function TileGrid({
|
||||
className="
|
||||
bg-scale-100 dark:bg-scale-300
|
||||
hover:bg-scale-200 hover:dark:bg-scale-400
|
||||
group flex h-full w-full flex-col rounded border px-6
|
||||
group flex h-full w-full flex-col rounded-xl border px-6
|
||||
py-6 shadow
|
||||
transition-all
|
||||
hover:shadow-lg"
|
||||
@@ -79,7 +79,7 @@ export default function TileGrid({
|
||||
className="
|
||||
bg-scale-100 dark:bg-scale-300
|
||||
hover:bg-scale-200 hover:dark:bg-scale-400
|
||||
group flex h-full w-full flex-col rounded border px-6
|
||||
group flex h-full w-full flex-col rounded-xl border px-6
|
||||
py-6 shadow
|
||||
transition-all
|
||||
hover:shadow-lg"
|
||||
|
||||
@@ -8,6 +8,7 @@ interface Types {
|
||||
h1: string | React.ReactNode
|
||||
subheader: string | React.ReactNode
|
||||
icon?: string
|
||||
className?: string
|
||||
title?: string
|
||||
image?: string | React.ReactNode
|
||||
footer?: React.ReactNode
|
||||
@@ -23,7 +24,12 @@ interface Types {
|
||||
}
|
||||
|
||||
const ProductHeaderCentered = (props: Types) => (
|
||||
<div className="container relative w-full mx-auto px-6 pt-4 pb-0 sm:px-16 xl:px-20">
|
||||
<div
|
||||
className={[
|
||||
'container relative w-full mx-auto px-6 pt-4 pb-0 sm:px-16 xl:px-20',
|
||||
props.className,
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex flex-col text-center items-center">
|
||||
{props.image && typeof props.image === 'string' ? (
|
||||
<div className="relative w-full max-w-[830px] mx-auto z-0 aspect-[2.3/1] -mb-8 md:-mb-12 lg:-mb-16">
|
||||
@@ -40,7 +46,7 @@ const ProductHeaderCentered = (props: Types) => (
|
||||
{props.image}
|
||||
</div>
|
||||
)}
|
||||
<div className="relative z-10 flex flex-col items-center space-y-2 mx-auto max-w-2xl">
|
||||
<div className="relative overflow-hidden w-full z-10 flex flex-col items-center space-y-2 mx-auto max-w-2xl">
|
||||
<div>
|
||||
{props.icon || props.title ? (
|
||||
<div className="mb-4 flex justify-center items-center gap-3">
|
||||
@@ -57,7 +63,7 @@ const ProductHeaderCentered = (props: Types) => (
|
||||
) : null}
|
||||
</div>
|
||||
<div className={[styles['appear-from-bottom']].join(' ')}>
|
||||
<h1 className="h1 text-3xl md:text-4xl tracking-[-1px]" key={`h1`}>
|
||||
<h1 className="h1 text-3xl md:text-4xl xl:!text-5xl tracking-[-1.5px]" key={`h1`}>
|
||||
{props.h1}
|
||||
</h1>
|
||||
<p className="p !text-scale-1000">{props.subheader}</p>
|
||||
@@ -65,7 +71,7 @@ const ProductHeaderCentered = (props: Types) => (
|
||||
<div className="flex flex-row md:flex-row pt-8 md:items-center">
|
||||
{props.cta && (
|
||||
<Link href={props.cta.link} as={props.cta.link}>
|
||||
<a>
|
||||
<a tabIndex={-1}>
|
||||
<Button size="medium" className="text-white">
|
||||
{props.cta.label ?? 'Start for free'}
|
||||
</Button>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export const links = [
|
||||
{
|
||||
text: 'Resources',
|
||||
description: 'View examples, curated content from the community, migration guides, and more.',
|
||||
url: '/docs/guides/resources',
|
||||
icon: 'M19.4994 17.9996V7.50107L19.4864 3L14.0723 3L4.5 3L4.5 18.75M4.5 18.75C4.5 19.9926 5.50736 21 6.75 21H19.5V18.5255L19.4871 16.5H14.0728H6.75C5.50736 16.5 4.5 17.5074 4.5 18.75ZM7.50724 3V9L9.75771 6.45713L12.0072 9V3H7.50724Z',
|
||||
},
|
||||
{
|
||||
text: 'Changelog',
|
||||
description: 'See the latest updates and product improvements.',
|
||||
url: '/changelog',
|
||||
icon: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z',
|
||||
},
|
||||
{
|
||||
text: 'Careers',
|
||||
description: 'Join the Supabase team and get involved.',
|
||||
url: '/careers',
|
||||
icon: 'M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
||||
},
|
||||
{
|
||||
text: 'Open Source',
|
||||
description: 'We support existing open source tools and communities wherever possible.',
|
||||
url: '/open-source',
|
||||
icon: 'M10.7607 3.08859C15.7723 2.38592 20.0295 5.94789 20.8525 10.7642C21.5964 15.1122 19.4634 19.1692 15.8778 20.9967L14.0736 16.084C15.129 15.4346 15.8232 14.4223 16.0198 13.0819C16.3702 10.7219 14.7442 8.47426 12.3801 8.25648C10.2228 8.04047 8.33304 9.61601 7.99823 11.7702L7.99761 11.7743C7.73427 13.5096 8.47082 15.1847 9.92827 16.0841L8.12693 21C6.34571 20.1066 4.88125 18.6129 3.97963 16.7387L3.97879 16.7369C3.15872 15.0395 3.07406 13.7081 3.01524 12.783C3.01012 12.7024 3.0052 12.6249 3 12.5506C3.10941 7.30353 6.59243 3.6764 10.7607 3.08859Z',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
export const links = [
|
||||
{
|
||||
text: 'Resources',
|
||||
description: 'View examples, curated content from the community, migration guides, and more.',
|
||||
url: '/docs/guides/resources',
|
||||
icon: 'M19.4994 17.9996V7.50107L19.4864 3L14.0723 3L4.5 3L4.5 18.75M4.5 18.75C4.5 19.9926 5.50736 21 6.75 21H19.5V18.5255L19.4871 16.5H14.0728H6.75C5.50736 16.5 4.5 17.5074 4.5 18.75ZM7.50724 3V9L9.75771 6.45713L12.0072 9V3H7.50724Z',
|
||||
},
|
||||
{
|
||||
text: 'Changelog',
|
||||
description: 'See the latest updates and product improvements.',
|
||||
url: '/changelog',
|
||||
icon: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z',
|
||||
},
|
||||
{
|
||||
text: 'Careers',
|
||||
description: 'Join the Supabase team and get involved.',
|
||||
url: '/careers',
|
||||
icon: 'M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
||||
},
|
||||
{
|
||||
text: 'Open Source',
|
||||
description: 'We support existing open source tools and communities wherever possible.',
|
||||
url: '/open-source',
|
||||
icon: 'M10.7607 3.08859C15.7723 2.38592 20.0295 5.94789 20.8525 10.7642C21.5964 15.1122 19.4634 19.1692 15.8778 20.9967L14.0736 16.084C15.129 15.4346 15.8232 14.4223 16.0198 13.0819C16.3702 10.7219 14.7442 8.47426 12.3801 8.25648C10.2228 8.04047 8.33304 9.61601 7.99823 11.7702L7.99761 11.7743C7.73427 13.5096 8.47082 15.1847 9.92827 16.0841L8.12693 21C6.34571 20.1066 4.88125 18.6129 3.97963 16.7387L3.97879 16.7369C3.15872 15.0395 3.07406 13.7081 3.01524 12.783C3.01012 12.7024 3.0052 12.6249 3 12.5506C3.10941 7.30353 6.59243 3.6764 10.7607 3.08859Z',
|
||||
},
|
||||
{
|
||||
text: 'Partners',
|
||||
description: 'Become a Supabase Partner and enable new business opportunities.',
|
||||
url: '/partners',
|
||||
icon: 'M12.0003 6.1488L10.5812 4.76334C8.68921 2.91613 5.62161 2.91613 3.72957 4.76334C1.98628 6.46532 1.84923 9.14165 3.31842 10.9968C3.44378 11.1551 3.77166 11.514 3.92041 11.6592M12.0003 6.1488L13.4193 4.76334C15.3113 2.91613 18.3789 2.91613 20.271 4.76334C22.163 6.61054 22.163 9.60546 20.271 11.4527L19.2017 12.5186M12.0003 6.1488L10.7542 7.39725C10.0214 8.13141 10.0219 9.32043 10.7554 10.0539C11.4894 10.7879 12.6793 10.7879 13.4133 10.0539L16.0693 7.39787M14.9131 13.4531L17.4954 16.1809M17.4954 16.1809C18.2089 16.9347 18.1764 18.1242 17.4226 18.8378C16.6693 19.5509 15.4807 19.5189 14.7669 18.7663L13.3188 17.2217M17.4954 16.1809C18.2086 16.9344 19.3982 16.9676 20.1516 16.2543C20.9051 15.5411 20.9376 14.3521 20.2244 13.5987L17.6415 10.8703M6.13555 14.7589L7.46387 13.4306M6.13555 14.7589C5.40193 15.4925 4.21251 15.4925 3.47889 14.7589C2.74528 14.0253 2.74528 12.8358 3.47889 12.1022L4.80722 10.7739C5.54084 10.0403 6.73026 10.0403 7.46388 10.7739C8.19749 11.5075 8.19749 12.6969 7.46387 13.4306M6.13555 14.7589C5.40193 15.4925 5.40193 16.6819 6.13555 17.4155C6.86916 18.1492 8.05859 18.1492 8.7922 17.4155M7.46387 13.4306C8.19749 12.6969 9.38691 12.6969 10.1205 13.4306C10.8541 14.1642 10.8541 15.3536 10.1205 16.0872M8.7922 17.4155L10.1205 16.0872M8.7922 17.4155C8.05859 18.1492 8.05859 19.3386 8.7922 20.0722C9.52582 20.8058 10.7152 20.8058 11.4489 20.0722L12.7772 18.7439C13.5108 18.0102 13.5108 16.8208 12.7772 16.0872C12.0436 15.3536 10.8541 15.3536 10.1205 16.0872',
|
||||
},
|
||||
{
|
||||
text: 'Integrations',
|
||||
description: 'Use your favorite tools with Supabase.',
|
||||
url: '/partners/integrations',
|
||||
icon: 'M6.87232 21.5743C9.09669 21.5743 10.8999 19.7711 10.8999 17.5467C10.8999 15.3223 9.09669 13.5191 6.87232 13.5191C4.64794 13.5191 2.84473 15.3223 2.84473 17.5467C2.84473 19.7711 4.64794 21.5743 6.87232 21.5743Z M17.127 3.67236V11.1724M20.877 7.42274H13.377M3.12305 3.67236H10.6231V11.1724H3.12305V3.67236ZM13.377 13.7966H20.877V21.2966H13.377V13.7966ZM10.8999 17.5467C10.8999 19.7711 9.09669 21.5743 6.87232 21.5743C4.64794 21.5743 2.84473 19.7711 2.84473 17.5467C2.84473 15.3223 4.64794 13.5191 6.87232 13.5191C9.09669 13.5191 10.8999 15.3223 10.8999 17.5467Z',
|
||||
},
|
||||
]
|
||||
@@ -47,6 +47,10 @@
|
||||
"text": "System Status",
|
||||
"url": "https://status.supabase.com/"
|
||||
},
|
||||
{
|
||||
"text": "Become a Partner",
|
||||
"url": "/partners"
|
||||
},
|
||||
{
|
||||
"text": "Integrations",
|
||||
"url": "/partners/integrations"
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { IconAward, IconCompass, IconDollarSign, IconGlobe, IconTrendingUp } from 'ui'
|
||||
|
||||
export default {
|
||||
metaTitle: 'Partner with Supabase',
|
||||
metaDescription: 'Become a Supabase Partner and enable new business opportunities.',
|
||||
heroSection: {
|
||||
title: 'Partners',
|
||||
h1: <span className="heading-gradient">Partner with Supabase</span>,
|
||||
subheader: (
|
||||
<>
|
||||
Apply to the Partners program to list your integration <br className="hidden md:block" /> in
|
||||
our marketplace and grow your business.
|
||||
</>
|
||||
),
|
||||
image: (
|
||||
<div className="relative z-10 flex items-center justify-center mb-4">
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl bg-gradient-to-br from-[#3ECF8E] via-[#576a61] to-[#3ecfb2] border border-brand-900 flex items-center justify-center"
|
||||
style={{
|
||||
boxShadow: '0 0 20px #25e7761f, 0 0 30px rgba(238, 240, 190, 0.1)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="text-scale-100 h-8 w-8 flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M12.0003 6.1488L10.5812 4.76334C8.68921 2.91613 5.62161 2.91613 3.72957 4.76334C1.98628 6.46532 1.84923 9.14165 3.31842 10.9968C3.44378 11.1551 3.77166 11.514 3.92041 11.6592M12.0003 6.1488L13.4193 4.76334C15.3113 2.91613 18.3789 2.91613 20.271 4.76334C22.163 6.61054 22.163 9.60546 20.271 11.4527L19.2017 12.5186M12.0003 6.1488L10.7542 7.39725C10.0214 8.13141 10.0219 9.32043 10.7554 10.0539C11.4894 10.7879 12.6793 10.7879 13.4133 10.0539L16.0693 7.39787M14.9131 13.4531L17.4954 16.1809M17.4954 16.1809C18.2089 16.9347 18.1764 18.1242 17.4226 18.8378C16.6693 19.5509 15.4807 19.5189 14.7669 18.7663L13.3188 17.2217M17.4954 16.1809C18.2086 16.9344 19.3982 16.9676 20.1516 16.2543C20.9051 15.5411 20.9376 14.3521 20.2244 13.5987L17.6415 10.8703M6.13555 14.7589L7.46387 13.4306M6.13555 14.7589C5.40193 15.4925 4.21251 15.4925 3.47889 14.7589C2.74528 14.0253 2.74528 12.8358 3.47889 12.1022L4.80722 10.7739C5.54084 10.0403 6.73026 10.0403 7.46388 10.7739C8.19749 11.5075 8.19749 12.6969 7.46387 13.4306M6.13555 14.7589C5.40193 15.4925 5.40193 16.6819 6.13555 17.4155C6.86916 18.1492 8.05859 18.1492 8.7922 17.4155M7.46387 13.4306C8.19749 12.6969 9.38691 12.6969 10.1205 13.4306C10.8541 14.1642 10.8541 15.3536 10.1205 16.0872M8.7922 17.4155L10.1205 16.0872M8.7922 17.4155C8.05859 18.1492 8.05859 19.3386 8.7922 20.0722C9.52582 20.8058 10.7152 20.8058 11.4489 20.0722L12.7772 18.7439C13.5108 18.0102 13.5108 16.8208 12.7772 16.0872C12.0436 15.3536 10.8541 15.3536 10.1205 16.0872"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
cta: {
|
||||
label: 'Become a Partner',
|
||||
link: 'https://forms.supabase.com/partner',
|
||||
},
|
||||
},
|
||||
oAuthApp: {
|
||||
steps: [
|
||||
{
|
||||
title: 'Register App',
|
||||
text: 'An OAuth app first needs to be registered with Supabase',
|
||||
},
|
||||
{
|
||||
title: 'Add OAuth2 Support',
|
||||
text: 'Use the OAuth2 protocol to access a users organization or project',
|
||||
},
|
||||
{
|
||||
title: 'Receive Tokens',
|
||||
text: "You'll receive a new access and refresh token",
|
||||
},
|
||||
{
|
||||
title: 'Control Projects',
|
||||
text: 'Use Supabase REST API to control projects and other settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
featureBlocks: [
|
||||
{
|
||||
title: 'Technical support',
|
||||
description: 'Access technical support to back your integrations and customer projects.',
|
||||
icon: <IconCompass strokeWidth={1.5} />,
|
||||
},
|
||||
{
|
||||
title: 'Expand your ecosystem',
|
||||
description: 'Offer your own products and services to Supabase customers.',
|
||||
icon: <IconGlobe strokeWidth={1.5} />,
|
||||
},
|
||||
{
|
||||
title: 'Business growth',
|
||||
description: 'Explore new revenue streams and growth potential.',
|
||||
icon: <IconDollarSign strokeWidth={1.5} />,
|
||||
},
|
||||
{
|
||||
title: 'Scale with us',
|
||||
description: 'Scale automatically with the power of open-source Postgres technology.',
|
||||
icon: <IconTrendingUp strokeWidth={1.5} />,
|
||||
},
|
||||
],
|
||||
featuredApps: [
|
||||
{
|
||||
name: 'Arengu',
|
||||
type: 'integration',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/integrations/arengu/arengu_logo.jpeg',
|
||||
},
|
||||
{
|
||||
name: 'Auth0',
|
||||
type: 'integration',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/integrations/auth0/auth0_dark.png?t=2023-07-19T19%3A13%3A04.189Z',
|
||||
},
|
||||
{
|
||||
name: 'Appsmith',
|
||||
type: 'integration',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/integrations/appsmith/appsmith-logo.png',
|
||||
},
|
||||
{
|
||||
name: 'CALDA',
|
||||
type: 'experts',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/experts/calda/calda_logo.jpeg',
|
||||
},
|
||||
{
|
||||
name: 'Morrow',
|
||||
type: 'experts',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/experts/morrow/morrow-logo.png',
|
||||
},
|
||||
{
|
||||
name: 'Voypost',
|
||||
type: 'experts',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/experts/voypost/33024474.png',
|
||||
},
|
||||
{
|
||||
name: 'Vercel',
|
||||
type: 'integration',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/integrations/vercel/vercel-icon.jpeg',
|
||||
},
|
||||
{
|
||||
name: 'Prisma',
|
||||
type: 'integration',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/integrations/prisma/prisma-icon.png',
|
||||
},
|
||||
{
|
||||
name: 'Cloudflare-workers',
|
||||
type: 'integration',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/integrations/cloudflare-integration/cloudflare_workers_logo.png?t=2023-07-21T11%3A07%3A47.005Z',
|
||||
},
|
||||
{
|
||||
name: 'Codesandbox',
|
||||
type: 'integration',
|
||||
logo: 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/integrations/codesandbox/codesandbox_logo.jpeg',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -590,11 +590,6 @@ module.exports = [
|
||||
source: '/docs/guides/database/introduction',
|
||||
destination: '/docs/guides/database',
|
||||
},
|
||||
{
|
||||
permanent: false,
|
||||
source: '/partners',
|
||||
destination: '/partners/integrations',
|
||||
},
|
||||
{
|
||||
permanent: false,
|
||||
source: '/learn-more',
|
||||
|
||||
@@ -52,7 +52,7 @@ function ExpertPartnersPage(props: Props) {
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<DefaultLayout>
|
||||
<DefaultLayout className="bg-scale-400 dark:bg-scale-100">
|
||||
<SectionContainer className="space-y-12">
|
||||
<div>
|
||||
<h1 className="h1">{meta_title}</h1>
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { range } from 'lodash'
|
||||
|
||||
import { useBreakpoint } from 'common'
|
||||
import { Badge, Button, IconCode, TextLink } from 'ui'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import InteractiveShimmerCard from '~/components/InteractiveShimmerCard'
|
||||
import ProductHeaderCentered from '~/components/Sections/ProductHeaderCentered'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
|
||||
import pageData from '~/data/partners'
|
||||
|
||||
const Partners = () => {
|
||||
const router = useRouter()
|
||||
const isSm = useBreakpoint()
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={pageData.metaTitle}
|
||||
description={pageData.metaDescription}
|
||||
openGraph={{
|
||||
title: pageData.metaTitle,
|
||||
description: pageData.metaDescription,
|
||||
url: `https://supabase.com/partners`,
|
||||
images: [
|
||||
{
|
||||
url: `https://supabase.com${router.basePath}/images/og/integrations.png`, // TODO
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<DefaultLayout>
|
||||
<div className="relative bg-scale-500 dark:bg-scale-100 overflow-hidden">
|
||||
<SectionContainer className="overflow-hidden pt-8 pb-12 md:pt-12">
|
||||
<ProductHeaderCentered {...pageData.heroSection} />
|
||||
</SectionContainer>
|
||||
<div className="relative z-20 w-full flex py-16 mb-16 -mt-10 md:mb-24 md:-mt-20 justify-center gap-2 overflow-hidden mx-auto max-w-4xl before:content[''] before:absolute before:inset-0 before:w-full before:bg-[linear-gradient(to_right,var(--colors-scale5)_0%,transparent_10%,transparent_90%,var(--colors-scale5)_100%)] dark:before:bg-[linear-gradient(to_right,var(--colors-scale1)_0%,transparent_10%,transparent_90%,var(--colors-scale1)_100%)] before:z-10">
|
||||
<div className="absolute w-32 h-32 md:w-40 md:h-40 rounded-full bg-scale-100/90 backdrop-blur-2xl backdrop-filter from-scale-200 to-scale-100 border-4 border-scale-100 shadow-xl top-1/2 -translate-x-1/2 left-1/2 -translate-y-1/2 flex items-center justify-center z-30">
|
||||
<div className="absolute inset-0 w-full h-full transform">
|
||||
<svg
|
||||
className="absolute inset-[-2px] transform animate-[transformSpin_3s_both_cubic-bezier(.5,.2,.5,.8)_infinite] opacity-90"
|
||||
viewBox="0 0 182 183"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M91 1C120.005 1 145.841 14.5702 162.505 35.708"
|
||||
stroke="url(#paint0_linear_4766_6117)"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_4766_6117"
|
||||
x1="107"
|
||||
y1="2.5"
|
||||
x2="151"
|
||||
y2="21.5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#1CF7C3" stop-opacity="0" />
|
||||
<stop offset="0.510417" stop-color="#1CF7C3" />
|
||||
<stop offset="1" stop-color="#1CF7C3" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<Image
|
||||
src="/images/supabase-logo-icon.svg"
|
||||
alt="Supabase icon"
|
||||
width={isSm ? 45 : 60}
|
||||
height={isSm ? 45 : 60}
|
||||
/>
|
||||
</div>
|
||||
{range(0, 3).map((_) => (
|
||||
<div className="flex gap-2 animate-marquee will-change-transform">
|
||||
{pageData.featuredApps.map((app) => (
|
||||
<div className="w-20 h-20 md:w-28 md:h-28 rounded-2xl bg-gradient-to-t from-scale-300 dark:from-scale-200 to-scale-100 border border-scale-200 flex items-center justify-center shadow-xl">
|
||||
<Image
|
||||
src={app.logo}
|
||||
alt={app.name}
|
||||
width={isSm ? 24 : 45}
|
||||
height={isSm ? 24 : 45}
|
||||
className="w-8 h-8 overflow-hidden rounded-full"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute inset-0 z-[1] bg-[linear-gradient(to_top,var(--colors-scale5)_40%,var(--colors-scale3)_90%)] dark:bg-[linear-gradient(to_top,var(--colors-scale1)_40%,#121212_90%)]" />
|
||||
</div>
|
||||
<SectionContainer>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-4">
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<h2 className="text-2xl sm:text-3xl xl:text-4xl tracking-[-.5px]">
|
||||
Explore our marketplace
|
||||
</h2>
|
||||
<p className="text-scale-1000 text-xs sm:text-sm lg:text-base py-3 lg:max-w-md">
|
||||
Discover how technology and consulting partners are already working with Supabase.
|
||||
</p>
|
||||
<TextLink
|
||||
url="https://supabase.com/docs/guides/platform/marketplace"
|
||||
label="View docs"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 lg:col-span-3 w-full max-w-4xl grid gap-8 rounded md:grid-cols-2">
|
||||
<InteractiveShimmerCard
|
||||
hasInnerShimmer={false}
|
||||
hasActiveOnHover={true}
|
||||
innerClassName="px-8 py-6 group flex flex-col gap-4"
|
||||
>
|
||||
<div className="bg-scale-300 mb-4 dark:bg-scale-500 text-scale-1200 group-hover:text-brand-900 flex h-12 w-12 items-center justify-center rounded-md border transition-all group-hover:scale-105">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-scale-1200 text-lg">Experts</h3>
|
||||
<p className="text-scale-900 text-sm">
|
||||
Find an expert to help build your next idea.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link href="/partners/experts">
|
||||
<a className="absolute inset-0" />
|
||||
</Link>
|
||||
</InteractiveShimmerCard>
|
||||
<InteractiveShimmerCard
|
||||
hasInnerShimmer={false}
|
||||
hasActiveOnHover={true}
|
||||
innerClassName="px-8 py-6 group flex flex-col gap-4"
|
||||
>
|
||||
<div className="bg-scale-300 mb-4 dark:bg-scale-500 text-scale-1200 group-hover:text-brand-900 flex h-12 w-12 items-center justify-center rounded-md border transition-all group-hover:scale-105">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-scale-1200 text-lg">Integrations</h3>
|
||||
<p className="text-scale-900 text-sm">Use your favorite tools with Supabase.</p>
|
||||
</div>
|
||||
<Link href="/partners/integrations">
|
||||
<a className="absolute inset-0" />
|
||||
</Link>
|
||||
</InteractiveShimmerCard>
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
<SectionContainer className="!py-0">
|
||||
<div className="border-b" />
|
||||
</SectionContainer>
|
||||
<SectionContainer>
|
||||
<h2 className="text-2xl sm:text-3xl xl:text-4xl text-center tracking-[-.5px]">
|
||||
Partner benefits
|
||||
</h2>
|
||||
<div className="grid mt-8 lg:mt-16 gap-8 rounded md:grid-cols-2 xl:grid-cols-4">
|
||||
{pageData.featureBlocks.map((item, i) => {
|
||||
return (
|
||||
<div
|
||||
className="group flex flex-col items-center text-center gap-4 px-8 py-6"
|
||||
key={i}
|
||||
>
|
||||
<div className="bg-brand-300 dark:bg-brand-500 text-brand-1200 group-hover:text-brand-800 dark:group-hover:text-brand-1000 flex h-12 w-12 items-center justify-center rounded-md border border-brand-800 dark:border-brand-800 transition-all group-hover:scale-105">
|
||||
{item.icon ? item.icon : <IconCode strokeWidth={2} />}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-scale-1200 text-lg">{item.title}</h3>
|
||||
<p className="text-scale-900 text-sm">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</SectionContainer>
|
||||
<div className="bg-scale-400 dark:bg-scale-100 border-t border-b">
|
||||
<SectionContainer className="flex flex-col gap-8">
|
||||
<div className="flex flex-col lg:flex-row gap-8 xl:gap-10 justify-between">
|
||||
<div className="w-full lg:w-1/2 gap-2 flex flex-col items-start">
|
||||
<Badge>Beta</Badge>
|
||||
<h2 className="text-3xl xl:text-4xl mt-2 max-w-[280px] sm:max-w-xs xl:max-w-[360px] tracking-[-1px]">
|
||||
Publish an OAuth App
|
||||
</h2>
|
||||
<p className="text-scale-900 mb-4 max-w-sm">
|
||||
Supabase lets you build a third-party app that can control organizations or
|
||||
projects programmatically.
|
||||
</p>
|
||||
<TextLink
|
||||
url="https://supabase.com/docs/guides/platform/oauth-apps/publish-an-oauth-app"
|
||||
label="Learn more"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full lg:w-1/2 border bg-scale-300 flex items-center justify-center aspect-video rounded-xl overflow-hidden">
|
||||
<Image
|
||||
src="/images/partners/register-oauth-app.svg"
|
||||
alt="Register app via API"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</div>
|
||||
<SectionContainer>
|
||||
<div className="flex flex-col text-center gap-4 py-8 items-center justify-center">
|
||||
<h2 className="heading-gradient text-2xl sm:text-3xl xl:text-4xl">
|
||||
Reach out to partner with Supabase
|
||||
</h2>
|
||||
<div className="w-full mt-4 flex items-center justify-center text-center gap-4">
|
||||
<Link href="https://forms.supabase.com/partner">
|
||||
<a tabIndex={-1}>
|
||||
<Button size="medium">Become a Partner</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</DefaultLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Partners
|
||||
@@ -86,6 +86,8 @@ function IntegrationPartnersPage(props: Props) {
|
||||
})
|
||||
}, [debouncedSearchTerm, router])
|
||||
|
||||
console.log(partners)
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
@@ -94,7 +96,7 @@ function IntegrationPartnersPage(props: Props) {
|
||||
openGraph={{
|
||||
title: meta_title,
|
||||
description: meta_description,
|
||||
url: `https://supabase.com/partners`,
|
||||
url: `https://supabase.com/partners/integrations`,
|
||||
images: [
|
||||
{
|
||||
url: `https://supabase.com${router.basePath}/images/og/integrations.png`, // TODO
|
||||
@@ -102,7 +104,7 @@ function IntegrationPartnersPage(props: Props) {
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<DefaultLayout>
|
||||
<DefaultLayout className="bg-scale-400 dark:bg-scale-100">
|
||||
<SectionContainer className="space-y-16">
|
||||
<div>
|
||||
<h1 className="h1">{meta_title}</h1>
|
||||
|
||||
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
@@ -0,0 +1,26 @@
|
||||
<svg width="759" height="440" viewBox="0 0 759 440" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_220_165478)">
|
||||
<path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M59.9667 234.773V280.102H13.9893V281.398H59.9667V326.727H13.9893V328.023H59.9667V374H61.2618V328.023H106.592V374H107.887V328.023H153.217V374H154.512V328.023H199.842V374H201.137V328.023H246.467V374H247.762V328.023H293.092V374H294.387V328.023H339.717V374H341.012V328.023H386.342V374H387.637V328.023H432.967V374H434.262V328.023H479.592V374H480.887V328.023H526.217V374H527.512V328.023H572.842V374H574.137V328.023H619.467V374H620.762V328.023H666.092V374H667.387V328.023H712.717V374H714.012V328.023H759.989V326.727H714.012V281.398H759.989V280.102H714.012V234.773H759.989V233.477H714.012V188.148H759.989V186.852H714.012V141.523H759.989V140.227H714.012V94.8976H759.989V93.6024H714.012V48.2726H759.989V46.9774H714.012V1.64755H759.989V0.352415H714.012V-44.9774H759.989V-46.2726H714.012V-91.6024H759.989V-92.8976H714.012V-138.227H759.989V-139.523H714.012V-184.852H759.989V-186.148H714.012V-231.477H759.989V-232.773H714.012V-278.102H759.989V-279.398H714.012V-324.727H759.989V-326.023H714.012V-372H712.717V-326.023H667.387V-372H666.092V-326.023H620.762V-372H619.467V-326.023H574.137V-372H572.842V-326.023H527.512V-372H526.217V-326.023H480.887V-372H479.592V-326.023H434.262V-372H432.967V-326.023H387.637V-372H386.342V-326.023H341.012V-372H339.717V-326.023H294.387V-372H293.092V-326.023H247.762V-372H246.467V-326.023H201.137V-372H199.842V-326.023H154.512V-372H153.217V-326.023H107.887V-372H106.592V-326.023H61.2618V-372H59.9667V-326.023H13.9893V-324.727H59.9667V-279.398H13.9893V-278.102H59.9667V-232.773H13.9893V-231.477H59.9667V-186.148H13.9893V-184.852H59.9667V-139.523H13.9893V-138.227H59.9667V-92.8976H13.9893V-91.6025H59.9667V-46.2726H13.9893V-44.9775H59.9667V0.352376H13.9893V1.64751H59.9667V46.9774H13.9893V48.2725H59.9667V93.6024H13.9893V94.8975H59.9667V140.227H13.9893V141.523H59.9667V186.852H13.9893V188.148H59.9667V233.477H13.9893V234.773H59.9667ZM712.717 326.727V281.398H667.387V326.727H712.717ZM666.092 326.727V281.398H620.762V326.727H666.092ZM619.467 326.727V281.398H574.137V326.727H619.467ZM572.842 326.727V281.398H527.512V326.727H572.842ZM526.217 326.727V281.398H480.887V326.727H526.217ZM479.592 326.727V281.398H434.262V326.727H479.592ZM432.967 326.727V281.398H387.637V326.727H432.967ZM386.342 326.727V281.398H341.012V326.727H386.342ZM339.717 326.727V281.398H294.387V326.727H339.717ZM293.092 326.727V281.398H247.762V326.727H293.092ZM246.467 326.727V281.398H201.137V326.727H246.467ZM199.842 326.727V281.398H154.512V326.727H199.842ZM153.217 326.727V281.398H107.887V326.727H153.217ZM106.592 326.727V281.398H61.2618V326.727H106.592ZM106.592 280.102H61.2618V234.773H106.592V280.102ZM153.217 280.102H107.887V234.773H153.217V280.102ZM199.842 280.102H154.512V234.773H199.842V280.102ZM246.467 280.102H201.137V234.773H246.467V280.102ZM293.092 280.102H247.762V234.773H293.092V280.102ZM339.717 280.102H294.387V234.773H339.717V280.102ZM386.342 280.102H341.012V234.773H386.342V280.102ZM432.967 280.102H387.637V234.773H432.967V280.102ZM479.592 280.102H434.262V234.773H479.592V280.102ZM526.217 280.102H480.887V234.773H526.217V280.102ZM572.842 280.102H527.512V234.773H572.842V280.102ZM619.467 280.102H574.137V234.773H619.467V280.102ZM666.092 280.102H620.762V234.773H666.092V280.102ZM712.717 280.102H667.387V234.773H712.717V280.102ZM712.717 -324.727H667.387V-279.398H712.717V-324.727ZM712.717 -278.102H667.387V-232.773H712.717V-278.102ZM712.717 -231.477H667.387V-186.148H712.717V-231.477ZM712.717 -184.852H667.387V-139.523H712.717V-184.852ZM712.717 -138.227H667.387V-92.8976H712.717V-138.227ZM712.717 -91.6025H667.387V-46.2726H712.717V-91.6025ZM712.717 -44.9775H667.387V0.352415H712.717V-44.9775ZM712.717 1.64755H667.387V46.9774H712.717V1.64755ZM712.717 48.2726H667.387V93.6024H712.717V48.2726ZM712.717 94.8976H667.387V140.227H712.717V94.8976ZM712.717 141.523H667.387V186.852H712.717V141.523ZM712.717 188.148H667.387V233.477H712.717V188.148ZM666.092 188.148V233.477H620.762V188.148H666.092ZM666.092 141.523V186.852H620.762V141.523H666.092ZM666.092 94.8976V140.227H620.762V94.8976H666.092ZM666.092 48.2726V93.6024H620.762V48.2726H666.092ZM666.092 1.64755V46.9774H620.762V1.64755H666.092ZM666.092 -44.9775V0.352415H620.762V-44.9775H666.092ZM666.092 -91.6025V-46.2726H620.762V-91.6025H666.092ZM666.092 -138.227V-92.8976H620.762V-138.227H666.092ZM666.092 -184.852V-139.523H620.762V-184.852H666.092ZM666.092 -231.477V-186.148H620.762V-231.477H666.092ZM666.092 -278.102V-232.773H620.762V-278.102H666.092ZM666.092 -324.727V-279.398H620.762V-324.727H666.092ZM61.2618 233.477H106.592V188.148H61.2618V233.477ZM107.887 233.477H153.217V188.148H107.887V233.477ZM154.512 233.477H199.842V188.148H154.512V233.477ZM201.137 233.477H246.467V188.148H201.137V233.477ZM247.762 233.477H293.092V188.148H247.762V233.477ZM294.387 233.477H339.717V188.148H294.387V233.477ZM341.012 233.477H386.342V188.148H341.012V233.477ZM387.637 233.477H432.967V188.148H387.637V233.477ZM434.262 233.477H479.592V188.148H434.262V233.477ZM480.887 233.477H526.217V188.148H480.887V233.477ZM527.512 233Line truncated
|
||||
<g filter="url(#filter0_f_220_165478)">
|
||||
<path d="M323.359 328.302C261.359 326.343 248.198 185.085 249.367 114.701C245.781 -23.1565 38.5496 -27.5879 119.608 -103.238C200.667 -178.887 268.875 -66.1342 419.283 -29.6888C569.691 6.75665 422.437 120.585 396.826 219.768C371.215 318.952 542.683 366.824 518.915 423.119C495.147 479.414 400.86 330.751 323.359 328.302Z" fill="url(#paint1_linear_220_165478)" fill-opacity="0.8"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_220_165478" x="-29.1807" y="-258.811" width="680.363" height="824.65" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="65" result="effect1_foregroundBlur_220_165478"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_220_165478" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(386.989 1) rotate(90) scale(338.641 417.448)">
|
||||
<stop stop-color="#0DFB90"/>
|
||||
<stop offset="1" stop-color="#7298F9" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_220_165478" x1="522.403" y1="297.973" x2="67.9033" y2="176.19" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#03C9B2" stop-opacity="0.37"/>
|
||||
<stop offset="1" stop-color="#0055B8"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_220_165478">
|
||||
<rect width="759" height="440" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.0003 6.1488L10.5812 4.76334C8.68921 2.91613 5.62161 2.91613 3.72957 4.76334C1.98628 6.46532 1.84923 9.14165 3.31842 10.9968C3.44378 11.1551 3.77166 11.514 3.92041 11.6592M12.0003 6.1488L13.4193 4.76334C15.3113 2.91613 18.3789 2.91613 20.271 4.76334C22.163 6.61054 22.163 9.60546 20.271 11.4527L19.2017 12.5186M12.0003 6.1488L10.7542 7.39725C10.0214 8.13141 10.0219 9.32043 10.7554 10.0539C11.4894 10.7879 12.6793 10.7879 13.4133 10.0539L16.0693 7.39787M14.9131 13.4531L17.4954 16.1809M17.4954 16.1809C18.2089 16.9347 18.1764 18.1242 17.4226 18.8378C16.6693 19.5509 15.4807 19.5189 14.7669 18.7663L13.3188 17.2217M17.4954 16.1809C18.2086 16.9344 19.3982 16.9676 20.1516 16.2543C20.9051 15.5411 20.9376 14.3521 20.2244 13.5987L17.6415 10.8703M6.13555 14.7589L7.46387 13.4306M6.13555 14.7589C5.40193 15.4925 4.21251 15.4925 3.47889 14.7589C2.74528 14.0253 2.74528 12.8358 3.47889 12.1022L4.80722 10.7739C5.54084 10.0403 6.73026 10.0403 7.46388 10.7739C8.19749 11.5075 8.19749 12.6969 7.46387 13.4306M6.13555 14.7589C5.40193 15.4925 5.40193 16.6819 6.13555 17.4155C6.86916 18.1492 8.05859 18.1492 8.7922 17.4155M7.46387 13.4306C8.19749 12.6969 9.38691 12.6969 10.1205 13.4306C10.8541 14.1642 10.8541 15.3536 10.1205 16.0872M8.7922 17.4155L10.1205 16.0872M8.7922 17.4155C8.05859 18.1492 8.05859 19.3386 8.7922 20.0722C9.52582 20.8058 10.7152 20.8058 11.4489 20.0722L12.7772 18.7439C13.5108 18.0102 13.5108 16.8208 12.7772 16.0872C12.0436 15.3536 10.8541 15.3536 10.1205 16.0872" stroke="#8F8F8F" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 405 KiB |
@@ -0,0 +1,84 @@
|
||||
<svg width="629" height="341" viewBox="0 0 629 341" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4769_1899)">
|
||||
<rect x="1" y="1" width="627" height="339" fill="#030A0C"/>
|
||||
<g opacity="0.3" filter="url(#filter0_f_4769_1899)">
|
||||
<ellipse cx="46.5853" cy="345.021" rx="231.808" ry="137.404" fill="url(#paint0_linear_4769_1899)"/>
|
||||
</g>
|
||||
<g opacity="0.3" filter="url(#filter1_f_4769_1899)">
|
||||
<ellipse cx="471.023" cy="218.748" rx="231.808" ry="137.404" fill="url(#paint1_linear_4769_1899)"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_b_4769_1899)">
|
||||
<rect x="56.1641" y="72.2578" width="596.836" height="269.605" rx="6.22999" fill="#0B1015"/>
|
||||
<rect x="55.9694" y="72.0631" width="597.225" height="269.995" rx="6.42468" stroke="url(#paint2_linear_4769_1899)" stroke-width="0.389375"/>
|
||||
</g>
|
||||
<path d="M211.024 83.1445L206.914 87.2543" stroke="#A7A7A7" stroke-width="0.389375"/>
|
||||
<path d="M211.023 87.2543L206.914 83.1445" stroke="#A7A7A7" stroke-width="0.389375"/>
|
||||
<g filter="url(#filter3_b_4769_1899)">
|
||||
<path d="M56.1641 78.4878C56.1641 75.0471 58.9533 72.2578 62.3941 72.2578H87.0859V345.757H56.1641V78.4878Z" fill="white" fill-opacity="0.02"/>
|
||||
</g>
|
||||
<path d="M73.6118 120.094V119.141H76.2229V111.79H76.1316L73.8338 113.932L73.1941 113.24L75.6093 110.981H77.3196V119.141H79.7218V120.094H73.6118Z" fill="white" fill-opacity="0.31"/>
|
||||
<path d="M79.513 139.43H73.4291V138.307L76.471 135.566C76.9149 135.165 77.2804 134.756 77.5677 134.338C77.8549 133.912 77.9985 133.464 77.9985 132.994V132.837C77.9985 132.289 77.8549 131.866 77.5677 131.571C77.2804 131.266 76.854 131.114 76.2882 131.114C75.7312 131.114 75.3004 131.257 74.9957 131.544C74.6998 131.823 74.4778 132.197 74.3299 132.667L73.3507 132.302C73.4378 132.032 73.5553 131.771 73.7032 131.518C73.8599 131.257 74.0557 131.027 74.2907 130.826C74.5257 130.626 74.8086 130.465 75.1393 130.343C75.4788 130.221 75.8704 130.161 76.3143 130.161C76.7669 130.161 77.1673 130.226 77.5154 130.356C77.8723 130.487 78.1682 130.67 78.4032 130.905C78.6469 131.14 78.8297 131.418 78.9516 131.74C79.0821 132.062 79.1474 132.415 79.1474 132.798C79.1474 133.146 79.0952 133.472 78.9907 133.777C78.895 134.082 78.7557 134.373 78.573 134.652C78.3989 134.93 78.1856 135.204 77.9332 135.474C77.6895 135.735 77.4154 135.996 77.1107 136.257L74.5649 138.477H79.513V139.43Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M76.0271 153.518C76.6625 153.518 77.1368 153.379 77.4502 153.1C77.7722 152.813 77.9332 152.447 77.9332 152.003V151.912C77.9332 151.425 77.7766 151.059 77.4632 150.815C77.1586 150.572 76.7582 150.45 76.2621 150.45C75.7747 150.45 75.3787 150.559 75.0741 150.776C74.7781 150.985 74.5344 151.264 74.3429 151.612L73.5204 150.985C73.6336 150.802 73.7728 150.624 73.9382 150.45C74.1036 150.267 74.2951 150.106 74.5127 149.967C74.739 149.828 74.9957 149.714 75.2829 149.627C75.5789 149.54 75.914 149.497 76.2882 149.497C76.6799 149.497 77.0454 149.549 77.3849 149.653C77.7243 149.749 78.0203 149.897 78.2727 150.097C78.5251 150.289 78.7209 150.528 78.8602 150.815C79.0081 151.103 79.0821 151.429 79.0821 151.795C79.0821 152.09 79.0342 152.356 78.9385 152.591C78.8515 152.826 78.7253 153.03 78.5599 153.205C78.4032 153.379 78.2161 153.527 77.9985 153.648C77.7809 153.77 77.5503 153.862 77.3066 153.923V153.975C77.559 154.027 77.8027 154.114 78.0377 154.236C78.2727 154.349 78.4816 154.501 78.6643 154.693C78.8471 154.876 78.9907 155.102 79.0952 155.372C79.2083 155.633 79.2649 155.933 79.2649 156.273C79.2649 156.664 79.1866 157.025 79.0299 157.356C78.8819 157.678 78.6687 157.957 78.3902 158.192C78.1204 158.418 77.7896 158.597 77.3979 158.727C77.015 158.858 76.5885 158.923 76.1185 158.923C75.7181 158.923 75.3613 158.879 75.0479 158.792C74.7433 158.705 74.4691 158.588 74.2254 158.44C73.9904 158.292 73.7816 158.127 73.5988 157.944C73.4247 157.761 73.268 157.569 73.1288 157.369L73.9513 156.743C74.0731 156.925 74.1993 157.091 74.3299 157.239C74.4604 157.387 74.6084 157.517 74.7738 157.63C74.9479 157.735 75.1437 157.818 75.3613 157.878C75.5789 157.939 75.8313 157.97 76.1185 157.97C76.7626 157.97 77.2543 157.822 77.5938 157.526C77.9419 157.23 78.116 156.808 78.116 156.26V156.155C78.116 155.615 77.9463 155.198 77.6068 154.902C77.2761 154.606 76.7756 154.458 76.1054 154.458H75.0218V153.518H76.0271Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M77.4371 178.103V176.314H72.946V175.348L76.8888 168.99H78.4816V175.413H79.8393V176.314H78.4816V178.103H77.4371ZM73.9252 175.413H77.4371V169.838H77.3849L73.9252 175.413Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M79.043 189.279H74.9696L74.7085 192.765H74.7999C75.0262 192.46 75.2786 192.221 75.5571 192.047C75.8356 191.864 76.2186 191.773 76.706 191.773C77.1064 191.773 77.4719 191.838 77.8027 191.968C78.1421 192.09 78.4337 192.273 78.6774 192.517C78.9211 192.76 79.1126 193.056 79.2518 193.405C79.3911 193.744 79.4607 194.136 79.4607 194.58C79.4607 195.023 79.3911 195.433 79.2518 195.807C79.1126 196.172 78.908 196.49 78.6382 196.76C78.3684 197.021 78.042 197.225 77.6591 197.373C77.2761 197.521 76.8409 197.595 76.3535 197.595C75.9705 197.595 75.6267 197.552 75.3221 197.465C75.0262 197.378 74.7607 197.26 74.5257 197.112C74.2994 196.964 74.0992 196.799 73.9252 196.616C73.7511 196.433 73.5944 196.242 73.4552 196.042L74.2777 195.415C74.3995 195.598 74.5214 195.763 74.6432 195.911C74.7738 196.059 74.9217 196.19 75.0871 196.303C75.2525 196.407 75.4396 196.49 75.6485 196.551C75.8574 196.612 76.1011 196.642 76.3796 196.642C77.0063 196.642 77.485 196.473 77.8157 196.133C78.1552 195.794 78.3249 195.324 78.3249 194.723V194.619C78.3249 194.018 78.1552 193.548 77.8157 193.209C77.4763 192.869 76.9889 192.7 76.3535 192.7C75.9183 192.7 75.5789 192.774 75.3352 192.921C75.1002 193.069 74.8826 193.252 74.6824 193.47L73.7554 193.339L74.0818 188.326H79.043V189.279Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M76.4318 216.932C75.9531 216.932 75.5223 216.853 75.1393 216.697C74.7564 216.531 74.43 216.301 74.1602 216.005C73.8991 215.7 73.6945 215.33 73.5466 214.895C73.4073 214.46 73.3377 213.972 73.3377 213.433C73.3377 212.754 73.4378 212.119 73.6379 211.527C73.8468 210.926 74.1079 210.382 74.4213 209.895C74.7433 209.399 75.1002 208.963 75.4918 208.589C75.8835 208.206 76.2665 207.897 76.6407 207.662H78.0507C77.5546 208.019 77.1107 208.367 76.7191 208.707C76.3361 209.046 75.9966 209.403 75.7007 209.777C75.4048 210.143 75.1567 210.539 74.9566 210.965C74.7564 211.383 74.591 211.857 74.4604 212.388L74.5257 212.414C74.7433 212.04 75.0305 211.731 75.3874 211.488C75.7442 211.235 76.2055 211.109 76.7713 211.109C77.1717 211.109 77.5372 211.174 77.8679 211.305C78.1987 211.435 78.4859 211.622 78.7296 211.866C78.9733 212.11 79.1605 212.406 79.291 212.754C79.4303 213.102 79.4999 213.489 79.4999 213.916C79.4999 214.36 79.4259 214.769 79.278 215.143C79.13 215.509 78.9211 215.826 78.6513 216.096C78.3902 216.357 78.0681 216.562 77.6852 216.71C77.3109 216.858 76.8931 216.932 76.4318 216.932ZM76.4188 216.005C77.0454 216.005 77.5285 215.835 77.8679 215.496C78.2161 215.156 78.3902 214.677 78.3902 214.059V213.955C78.3902 213.337 78.2161 212.858 77.8679 212.519C77.5285 212.179 77.0454 212.01 76.4188 212.01C75.7921 212.01 75.3047 212.179 74.9566 212.519C74.6171 212.858 74.4474 213.337 74.4474 213.955V214.059C74.4474 214.677 74.6171 215.156 74.9566 215.496C75.3047 215.835 75.7921 216.005 76.4188 216.005Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M74.7607 236.111L78.3249 227.938H74.3299V229.636H73.3507V226.998H79.4346V227.965L75.9357 236.111H74.7607Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M76.4188 255.604C75.8879 255.604 75.4222 255.539 75.0218 255.408C74.6215 255.269 74.2864 255.082 74.0166 254.847C73.7554 254.612 73.5553 254.333 73.416 254.011C73.2854 253.689 73.2202 253.341 73.2202 252.967C73.2202 252.34 73.3899 251.844 73.7293 251.479C74.0688 251.113 74.517 250.856 75.0741 250.708V250.604C74.5866 250.439 74.1993 250.173 73.9121 249.807C73.6336 249.433 73.4943 248.985 73.4943 248.463C73.4943 247.766 73.7511 247.214 74.2646 246.805C74.7781 246.387 75.4962 246.178 76.4188 246.178C77.3414 246.178 78.0594 246.387 78.573 246.805C79.0865 247.214 79.3432 247.766 79.3432 248.463C79.3432 248.985 79.1996 249.433 78.9124 249.807C78.6339 250.173 78.2509 250.439 77.7635 250.604V250.708C78.3205 250.856 78.7688 251.113 79.1082 251.479C79.4477 251.844 79.6174 252.34 79.6174 252.967C79.6174 253.341 79.5478 253.689 79.4085 254.011C79.278 254.333 79.0778 254.612 78.808 254.847C78.5381 255.082 78.203 255.269 77.8027 255.408C77.411 255.539 76.9497 255.604 76.4188 255.604ZM76.4188 254.677C77.0629 254.677 77.5677 254.534 77.9332 254.246C78.2988 253.959 78.4816 253.554 78.4816 253.032V252.745C78.4816 252.223 78.2988 251.818 77.9332 251.531C77.5764 251.244 77.0716 251.1 76.4188 251.1C75.766 251.1 75.2568 251.244 74.8913 251.531C74.5344 251.818 74.356 252.223 74.356 252.745V253.032C74.356 253.554 74.5388 253.959 74.9043 254.246C75.2699 254.534 75.7747 254.677 76.4188 254.677ZM76.4188 250.212C77.0106 250.212 77.4589 250.086 77.7635 249.834C78.0768 249.581 78.2335 249.22 78.2335 248.75V248.567C78.2335 248.097 78.0725 247.736 77.7504 247.484C77.4371 247.231 76.9932 247.105 76.4188 247.105C75.8443 247.105 75.3961 247.231 75.0741 247.484C74.7607 247.736 74.6041 248.097 74.6041 248.567V248.75C74.6041 249.22 74.7564 249.581 75.061 249.834C75.3743 250.086 75.8269 250.212 76.4188 250.212Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M79.4999 269.013C79.4999 269.692 79.3955 270.332 79.1866 270.932C78.9864 271.524 78.7253 272.068 78.4032 272.564C78.0812 273.052 77.7243 273.487 77.3327 273.87C76.9497 274.244 76.5711 274.549 76.1968 274.784H74.7868C75.2829 274.427 75.7225 274.079 76.1054 273.739C76.4971 273.4 76.8409 273.047 77.1368 272.682C77.4328 272.307 77.6808 271.911 77.881 271.494C78.0812 271.067 78.2466 270.588 78.3771 270.058L78.3118 270.031C78.0855 270.406 77.794 270.719 77.4371 270.971C77.089 271.215 76.632 271.337 76.0663 271.337C75.6659 271.337 75.296 271.272 74.9566 271.141C74.6258 271.011 74.3386 270.823 74.0949 270.58C73.8599 270.336 73.6728 270.045 73.5335 269.705C73.4029 269.357 73.3377 268.965 73.3377 268.53C73.3377 268.086 73.4073 267.681 73.5466 267.316C73.6945 266.942 73.9034 266.624 74.1732 266.363C74.443 266.093 74.7651 265.884 75.1393 265.736C75.5223 265.588 75.9444 265.514 76.4057 265.514C76.8844 265.514 77.3153 265.597 77.6982 265.762C78.0812 265.919 78.4032 266.15 78.6643 266.454C78.9342 266.75 79.1387 267.116 79.278 267.551C79.4259 267.986 79.4999 268.473 79.4999 269.013ZM76.4188 270.436C77.0454 270.436 77.5285 270.266 77.8679 269.927C78.2161 269.588 78.3902 269.109 78.3902 268.491V268.386C78.3902 267.768 78.2161 267.29 77.8679 266.95C77.5285 266.611 77.0454 266.441 76.4188 266.441C75.7921 266.441 75.3047 266.611 74.9566 266.95C74.6171 267.29 74.4474 267.768 74.4474 268.386V268.491C74.4474 269.109 74.6171 269.588 74.9566 269.927C75.3047 270.266 75.7921 270.436 76.4188 270.436Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M65.6977 294.12V293.167H68.3088V285.817H68.2174L65.9197 287.958L65.2799 287.266L67.6952 285.007H69.4055V293.167H71.8077V294.12H65.6977ZM76.3329 294.277C75.7759 294.277 75.2972 294.172 74.8968 293.963C74.4964 293.746 74.1657 293.437 73.9046 293.036C73.6435 292.636 73.452 292.144 73.3301 291.561C73.2083 290.978 73.1473 290.312 73.1473 289.564C73.1473 288.824 73.2083 288.162 73.3301 287.579C73.452 286.987 73.6435 286.491 73.9046 286.091C74.1657 285.69 74.4964 285.386 74.8968 285.177C75.2972 284.959 75.7759 284.851 76.3329 284.851C76.8899 284.851 77.3686 284.959 77.769 285.177C78.1694 285.386 78.5001 285.69 78.7612 286.091C79.0224 286.491 79.2138 286.987 79.3357 287.579C79.4575 288.162 79.5185 288.824 79.5185 289.564C79.5185 290.312 79.4575 290.978 79.3357 291.561C79.2138 292.144 79.0224 292.636 78.7612 293.036C78.5001 293.437 78.1694 293.746 77.769 293.963C77.3686 294.172 76.8899 294.277 76.3329 294.277ZM76.3329 293.324C76.6985 293.324 77.0118 293.254 77.2729 293.115C77.534 292.967 77.7429 292.762 77.8996 292.501C78.0649 292.24 78.1868 291.927 78.2651 291.561C78.3435 291.187 78.3826 290.769 78.3826 290.308V288.819C78.3826 288.367 78.3435 287.953 78.2651 287.579C78.1868 287.205 78.0649 286.887 77.8996 286.626C77.7429 286.365 77.534 286.165 77.2729 286.026C77.0118 285.878 76.6985 285.804 76.3329 285.804C75.9673 285.804 75.654 285.878 75.3929 286.026C75.1318 286.165 74.9185 286.365 74.7532 286.626C74.5965 286.887 74.479 287.205 74.4007 287.579C74.3223 287.953 74.2832 288.367 74.2832 288.819V290.308C74.2832 290.769 74.3223 291.187 74.4007 291.561C74.479 291.927 74.5965 292.24 74.7532 292.501C74.9185 292.762 75.1318 292.967 75.3929 293.115C75.654 293.254 75.9673 293.324 76.3329 293.324ZM76.3329 290.334C76.0283 290.334 75.815 290.273 75.6932 290.151C75.58 290.029 75.5235 289.881 75.5235 289.707V289.42C75.5235 289.246 75.58 289.098 75.6932 288.976C75.815 288.854 76.0283 288.793 76.3329 288.793C76.6375 288.793 76.8464 288.854 76.9596 288.976C77.0814 289.098 77.1423 289.246 77.1423 289.42V289.707C77.1423 289.881 77.0814 290.029 76.9596 290.151C76.8464 290.273 76.6375 290.334 76.3329 290.334Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M65.6977 313.456V312.503H68.3088V305.153H68.2174L65.9197 307.294L65.2799 306.602L67.6952 304.343H69.4055V312.503H71.8077V313.456H65.6977ZM73.526 313.456V312.503H76.1371V305.153H76.0457L73.7479 307.294L73.1082 306.602L75.5235 304.343H77.2337V312.503H79.636V313.456H73.526Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M65.6977 332.792V331.839H68.3088V324.489H68.2174L65.9197 326.63L65.2799 325.938L67.6952 323.68H69.4055V331.839H71.8077V332.792H65.6977ZM79.4271 332.792H73.3432V331.67L76.3851 328.928C76.829 328.528 77.1946 328.119 77.4818 327.701C77.769 327.274 77.9126 326.826 77.9126 326.356V326.199C77.9126 325.651 77.769 325.229 77.4818 324.933C77.1946 324.628 76.7681 324.476 76.2023 324.476C75.6453 324.476 75.2145 324.62 74.9098 324.907C74.6139 325.185 74.392 325.56 74.244 326.03L73.2648 325.664C73.3519 325.394 73.4694 325.133 73.6173 324.881C73.774 324.62 73.9698 324.389 74.2048 324.189C74.4398 323.989 74.7227 323.828 75.0535 323.706C75.3929 323.584 75.7846 323.523 76.2285 323.523C76.6811 323.523 77.0814 323.588 77.4296 323.719C77.7864 323.849 78.0824 324.032 78.3174 324.267C78.5611 324.502 78.7438 324.781 78.8657 325.103C78.9962 325.425 79.0615 325.777 79.0615 326.16C79.0615 326.508 79.0093 326.835 78.9049 327.139C78.8091 327.444 78.6699 327.736 78.4871 328.014C78.313 328.293 78.0998 328.567 77.8474 328.837C77.6036 329.098 77.3295 329.359 77.0248 329.62L74.479 331.839H79.4271V332.792Z" fill="white" fill-opacity="0.06"/>
|
||||
<path d="M106.144 119.477C105.656 119.477 105.221 119.394 104.838 119.229C104.464 119.055 104.146 118.815 103.885 118.511C103.624 118.206 103.424 117.836 103.285 117.401C103.154 116.966 103.089 116.483 103.089 115.952C103.089 115.421 103.158 114.938 103.298 114.503C103.437 114.068 103.637 113.698 103.898 113.393C104.159 113.08 104.477 112.84 104.851 112.675C105.226 112.51 105.652 112.427 106.131 112.427C106.784 112.427 107.315 112.571 107.724 112.858C108.133 113.145 108.433 113.519 108.624 113.981L107.789 114.424C107.667 114.076 107.462 113.807 107.175 113.615C106.897 113.424 106.549 113.328 106.131 113.328C105.826 113.328 105.552 113.38 105.308 113.484C105.073 113.58 104.873 113.719 104.708 113.902C104.542 114.085 104.416 114.303 104.329 114.555C104.251 114.807 104.212 115.082 104.212 115.378V116.526C104.212 116.822 104.251 117.097 104.329 117.349C104.416 117.601 104.542 117.819 104.708 118.002C104.873 118.184 105.078 118.328 105.321 118.433C105.565 118.528 105.844 118.576 106.157 118.576C106.61 118.576 106.984 118.472 107.28 118.263C107.576 118.054 107.811 117.767 107.985 117.401L108.729 117.91C108.529 118.363 108.215 118.737 107.789 119.033C107.371 119.329 106.823 119.477 106.144 119.477ZM115.33 118.224H115.278C115.208 118.389 115.121 118.55 115.017 118.707C114.912 118.855 114.782 118.985 114.625 119.098C114.477 119.212 114.298 119.303 114.09 119.373C113.881 119.442 113.637 119.477 113.358 119.477C112.653 119.477 112.088 119.255 111.661 118.811C111.235 118.359 111.022 117.723 111.022 116.905V112.584H112.066V116.722C112.066 117.34 112.201 117.802 112.471 118.106C112.741 118.402 113.137 118.55 113.659 118.55C113.868 118.55 114.072 118.524 114.272 118.472C114.473 118.419 114.651 118.341 114.808 118.237C114.964 118.132 115.091 118.002 115.186 117.845C115.282 117.68 115.33 117.484 115.33 117.258V112.584H116.374V119.32H115.33V118.224ZM118.654 118.433H120.625V113.471H118.654V112.584H121.67V114.281H121.735C121.874 113.75 122.135 113.337 122.518 113.041C122.91 112.736 123.406 112.584 124.007 112.584H124.947V113.628H123.68C123.08 113.628 122.592 113.802 122.218 114.15C121.853 114.498 121.67 114.955 121.67 115.521V118.433H124.281V119.32H118.654V118.433ZM126.521 118.433H128.871V110.547H126.521V109.659H129.916V118.433H132.266V119.32H126.521V118.433Z" fill="#1CF7C3"/>
|
||||
<path d="M165.793 119.32V110.208H169.109C169.954 110.208 170.598 110.438 171.042 110.899C171.485 111.361 171.707 112.005 171.707 112.832C171.707 113.659 171.485 114.303 171.042 114.764C170.598 115.225 169.954 115.456 169.109 115.456H166.89V119.32H165.793ZM166.89 114.503H169.083C169.544 114.503 169.901 114.39 170.154 114.163C170.415 113.928 170.545 113.598 170.545 113.171V112.492C170.545 112.066 170.415 111.739 170.154 111.513C169.901 111.278 169.544 111.161 169.083 111.161H166.89V114.503ZM176.363 119.477C175.806 119.477 175.327 119.373 174.927 119.164C174.527 118.946 174.196 118.637 173.935 118.237C173.674 117.836 173.482 117.345 173.36 116.761C173.238 116.178 173.178 115.512 173.178 114.764C173.178 114.024 173.238 113.363 173.36 112.779C173.482 112.188 173.674 111.692 173.935 111.291C174.196 110.891 174.527 110.586 174.927 110.377C175.327 110.16 175.806 110.051 176.363 110.051C176.92 110.051 177.399 110.16 177.799 110.377C178.2 110.586 178.53 110.891 178.791 111.291C179.053 111.692 179.244 112.188 179.366 112.779C179.488 113.363 179.549 114.024 179.549 114.764C179.549 115.512 179.488 116.178 179.366 116.761C179.244 117.345 179.053 117.836 178.791 118.237C178.53 118.637 178.2 118.946 177.799 119.164C177.399 119.373 176.92 119.477 176.363 119.477ZM176.363 118.524C176.729 118.524 177.038 118.454 177.29 118.315C177.542 118.167 177.751 117.963 177.917 117.701C178.082 117.44 178.2 117.127 178.269 116.761C178.348 116.387 178.387 115.969 178.387 115.508V114.02C178.387 113.567 178.348 113.154 178.269 112.779C178.2 112.405 178.082 112.088 177.917 111.826C177.751 111.565 177.542 111.365 177.29 111.226C177.038 111.078 176.729 111.004 176.363 111.004C175.998 111.004 175.689 111.078 175.436 111.226C175.184 111.365 174.975 111.565 174.809 111.826C174.644 112.088 174.522 112.405 174.444 112.779C174.374 113.154 174.339 113.567 174.339 114.02V115.508C174.339 115.969 174.374 116.387 174.444 116.761C174.522 117.127 174.644 117.44 174.809 117.701C174.975 117.963 175.184 118.167 175.436 118.315C175.689 118.454 175.998 118.524 176.363 118.524ZM184.165 119.477C183.347 119.477 182.677 119.329 182.155 119.033C181.632 118.728 181.202 118.341 180.862 117.871L181.659 117.218C182.015 117.662 182.39 117.993 182.781 118.211C183.182 118.428 183.656 118.537 184.204 118.537C184.866 118.537 185.371 118.385 185.719 118.08C186.067 117.775 186.241 117.349 186.241 116.801C186.241 116.357 186.115 116.013 185.862 115.769C185.61 115.517 185.171 115.338 184.544 115.234L183.526 115.064C183.082 114.986 182.707 114.868 182.403 114.712C182.107 114.555 181.867 114.368 181.685 114.15C181.502 113.924 181.371 113.68 181.293 113.419C181.215 113.149 181.176 112.871 181.176 112.584C181.176 111.748 181.45 111.117 181.998 110.691C182.546 110.264 183.286 110.051 184.217 110.051C184.949 110.051 185.562 110.173 186.058 110.416C186.563 110.66 186.968 111.004 187.272 111.448L186.502 112.114C186.25 111.783 185.945 111.513 185.588 111.304C185.231 111.095 184.77 110.991 184.204 110.991C183.586 110.991 183.112 111.121 182.781 111.383C182.451 111.644 182.285 112.035 182.285 112.558C182.285 112.967 182.407 113.302 182.651 113.563C182.903 113.815 183.351 113.998 183.996 114.111L184.975 114.281C185.419 114.359 185.793 114.477 186.097 114.633C186.402 114.79 186.646 114.977 186.829 115.195C187.02 115.412 187.155 115.656 187.233 115.926C187.312 116.196 187.351 116.479 187.351 116.774C187.351 117.619 187.072 118.28 186.515 118.759C185.967 119.238 185.184 119.477 184.165 119.477ZM192.568 111.161V119.32H191.471V111.161H188.429V110.208H195.61V111.161H192.568ZM209.478 121.122L204.934 109.398H205.874L210.418 121.122H209.478ZM128.897 132.276V128.659H129.89V132.276H128.897ZM137.522 138.477C136.408 138.477 135.577 138.072 135.029 137.263C134.489 136.453 134.219 135.287 134.219 133.764C134.219 132.241 134.489 131.074 135.029 130.265C135.577 129.456 136.408 129.051 137.522 129.051C137.94 129.051 138.301 129.107 138.606 129.221C138.91 129.334 139.171 129.486 139.389 129.678C139.607 129.869 139.789 130.087 139.937 130.33C140.085 130.565 140.216 130.805 140.329 131.048L139.389 131.479C139.311 131.279 139.219 131.092 139.115 130.918C139.01 130.735 138.884 130.578 138.736 130.448C138.597 130.309 138.427 130.2 138.227 130.121C138.036 130.043 137.801 130.004 137.522 130.004C136.8 130.004 136.26 130.278 135.903 130.826C135.555 131.375 135.381 132.106 135.381 133.02V134.508C135.381 135.422 135.555 136.153 135.903 136.701C136.26 137.25 136.8 137.524 137.522 137.524C137.801 137.524 138.036 137.485 138.227 137.406C138.427 137.328 138.597 137.224 138.736 137.093C138.884 136.954 139.01 136.797 139.115 136.623C139.219 136.44 139.311 136.249 139.389 136.049L140.329 136.479C140.216 136.732 140.085 136.976 139.937 137.211C139.789 137.446 139.607 137.659 139.389 137.85C139.171 138.042 138.91 138.194 138.606 138.307C138.301 138.42 137.94 138.477 137.522 138.477ZM145.05 138.477C144.58 138.477 144.154 138.394 143.771 138.229C143.396 138.064 143.074 137.829 142.805 137.524C142.543 137.211 142.343 136.841 142.2Line truncated
|
||||
<path d="M143.157 115.9V114.803H146.943V115.9H143.157ZM156.469 119.32H155.228L154.145 117.506L152.944 115.521H152.891L151.651 117.506L150.502 119.32H149.301L152.278 114.673L149.471 110.208H150.711L151.769 111.97L152.904 113.811H152.957L154.079 111.97L155.176 110.208H156.377L153.557 114.659L156.469 119.32Z" fill="#3DCB8C"/>
|
||||
<path d="M104.016 134.9V133.803H107.802V134.9H104.016ZM115.513 134.182H111.962V138.32H110.865V129.208H111.962V133.229H115.513V129.208H116.609V138.32H115.513V134.182ZM104.016 154.9V153.803H107.802V154.9H104.016ZM115.513 154.182H111.962V158.32H110.865V149.208H111.962V153.229H115.513V149.208H116.609V158.32H115.513V154.182ZM104.016 173.9V172.803H107.802V173.9H104.016ZM115.369 176.224H115.317C114.882 177.059 114.19 177.477 113.241 177.477C112.414 177.477 111.766 177.168 111.296 176.55C110.834 175.932 110.604 175.066 110.604 173.952C110.604 172.838 110.834 171.972 111.296 171.354C111.766 170.736 112.414 170.427 113.241 170.427C114.19 170.427 114.882 170.845 115.317 171.68H115.369V167.659H116.414V177.32H115.369V176.224ZM113.646 176.55C113.881 176.55 114.103 176.52 114.312 176.459C114.52 176.398 114.703 176.311 114.86 176.198C115.017 176.076 115.138 175.932 115.225 175.767C115.321 175.593 115.369 175.392 115.369 175.166V172.738C115.369 172.512 115.321 172.316 115.225 172.15C115.138 171.976 115.017 171.833 114.86 171.719C114.703 171.598 114.52 171.506 114.312 171.445C114.103 171.384 113.881 171.354 113.646 171.354C113.036 171.354 112.562 171.541 112.223 171.915C111.892 172.281 111.727 172.768 111.727 173.378V174.526C111.727 175.136 111.892 175.627 112.223 176.002C112.562 176.367 113.036 176.55 113.646 176.55Z" fill="#E9E9E9"/>
|
||||
<line x1="244.545" y1="105.066" x2="244.545" y2="126.5" stroke="#848788" stroke-width="0.934499"/>
|
||||
<path d="M371.906 162.134V147.828L382.278 158.2H376.198L375.84 158.289L371.906 162.134Z" fill="#91C2DA"/>
|
||||
<path d="M380.042 162.761L376.823 164.102L372.621 154.177L375.929 152.836L380.042 162.761Z" fill="#91C2DA"/>
|
||||
<path d="M376.086 155.534L374.438 156.227L377.206 162.822L378.855 162.13L376.086 155.534Z" fill="#0D1118"/>
|
||||
<path d="M372.801 149.977V159.991L375.483 157.398L375.841 157.308H380.133L372.801 149.977Z" fill="#0D1118"/>
|
||||
<path opacity="0.9" d="M647.872 72.4102H234V95.7726H647.872V72.4102Z" fill="#131620" stroke="#1B1D29" stroke-width="0.778749"/>
|
||||
<path d="M113.916 81.4447C112.858 81.4447 112 82.0167 112 82.7223C112 83.428 112.858 84 113.916 84C114.975 84 115.833 84.572 115.833 85.2777C115.833 85.9833 114.975 86.5553 113.917 86.5553M113.916 81.4447C114.626 81.4447 115.245 81.7016 115.577 82.0835M113.916 81.4447V80.8058M113.916 81.4447L113.917 86.5553M113.917 86.5553L113.916 87.1942M113.917 86.5553C113.207 86.5553 112.588 86.2984 112.256 85.9165" stroke="#8F8F8F" stroke-linecap="round"/>
|
||||
<path d="M120.324 86.5V82.2523H120.955V82.8939H120.999C121.076 82.6837 121.216 82.5132 121.419 82.3823C121.622 82.2514 121.851 82.1859 122.105 82.1859C122.153 82.1859 122.213 82.1868 122.285 82.1887C122.357 82.1905 122.411 82.1933 122.448 82.197V82.8607C122.426 82.8551 122.375 82.8468 122.296 82.8358C122.218 82.8229 122.136 82.8164 122.05 82.8164C121.843 82.8164 121.659 82.8598 121.497 82.9464C121.336 83.0312 121.209 83.1492 121.115 83.3004C121.023 83.4497 120.977 83.6203 120.977 83.812V86.5H120.324ZM124.873 86.5885C124.464 86.5885 124.111 86.4982 123.814 86.3175C123.519 86.135 123.291 85.8805 123.131 85.5542C122.972 85.2261 122.893 84.8444 122.893 84.4093C122.893 83.9742 122.972 83.5908 123.131 83.2589C123.291 82.9252 123.514 82.6653 123.8 82.479C124.088 82.291 124.423 82.197 124.807 82.197C125.028 82.197 125.247 82.2338 125.462 82.3076C125.678 82.3813 125.874 82.5012 126.051 82.6671C126.228 82.8312 126.369 83.0487 126.474 83.3197C126.58 83.5908 126.632 83.9244 126.632 84.3208V84.5974H123.358V84.0332H125.968C125.968 83.7936 125.92 83.5797 125.825 83.3916C125.731 83.2036 125.596 83.0552 125.421 82.9464C125.248 82.8376 125.043 82.7832 124.807 82.7832C124.547 82.7832 124.322 82.8478 124.132 82.9768C123.944 83.104 123.799 83.27 123.698 83.4746C123.597 83.6792 123.546 83.8986 123.546 84.1328V84.5089C123.546 84.8297 123.601 85.1016 123.712 85.3247C123.824 85.5459 123.98 85.7146 124.179 85.8308C124.378 85.9451 124.61 86.0022 124.873 86.0022C125.045 86.0022 125.2 85.9783 125.338 85.9303C125.478 85.8805 125.599 85.8068 125.7 85.7091C125.802 85.6095 125.88 85.486 125.935 85.3385L126.566 85.5155C126.499 85.7294 126.388 85.9174 126.231 86.0797C126.074 86.24 125.881 86.3654 125.65 86.4558C125.42 86.5442 125.161 86.5885 124.873 86.5885ZM129.339 88.1814C129.024 88.1814 128.753 88.1408 128.526 88.0597C128.3 87.9804 128.111 87.8753 127.959 87.7445C127.81 87.6154 127.691 87.4771 127.603 87.3296L128.123 86.9646C128.182 87.042 128.256 87.1305 128.347 87.2301C128.437 87.3315 128.561 87.419 128.717 87.4928C128.876 87.5684 129.083 87.6062 129.339 87.6062C129.682 87.6062 129.965 87.5232 130.188 87.3573C130.412 87.1914 130.523 86.9314 130.523 86.5774V85.7146H130.468C130.42 85.792 130.352 85.8879 130.263 86.0022C130.176 86.1147 130.051 86.2152 129.887 86.3037C129.725 86.3903 129.505 86.4336 129.229 86.4336C128.886 86.4336 128.578 86.3525 128.305 86.1903C128.034 86.028 127.819 85.792 127.661 85.4823C127.504 85.1726 127.426 84.7965 127.426 84.354C127.426 83.9189 127.502 83.5401 127.655 83.2174C127.808 82.8929 128.021 82.6422 128.294 82.4652C128.567 82.2864 128.882 82.197 129.24 82.197C129.516 82.197 129.736 82.2431 129.898 82.3352C130.062 82.4256 130.188 82.5288 130.274 82.645C130.363 82.7593 130.431 82.8533 130.479 82.927H130.545V82.2523H131.176V86.6217C131.176 86.9867 131.093 87.2835 130.927 87.5122C130.763 87.7426 130.541 87.9113 130.263 88.0182C129.987 88.127 129.679 88.1814 129.339 88.1814ZM129.317 85.8474C129.579 85.8474 129.8 85.7874 129.981 85.6676C130.162 85.5478 130.299 85.3754 130.393 85.1505C130.487 84.9255 130.534 84.6564 130.534 84.343C130.534 84.0369 130.488 83.7668 130.396 83.5327C130.304 83.2985 130.167 83.1151 129.987 82.9824C129.806 82.8496 129.583 82.7832 129.317 82.7832C129.041 82.7832 128.81 82.8533 128.626 82.9934C128.443 83.1335 128.306 83.3216 128.214 83.5576C128.124 83.7936 128.078 84.0553 128.078 84.343C128.078 84.6379 128.124 84.8988 128.217 85.1256C128.311 85.3505 128.449 85.5275 128.631 85.6565C128.816 85.7837 129.044 85.8474 129.317 85.8474ZM132.37 86.5V82.2523H133.023V86.5H132.37ZM132.702 81.5443C132.575 81.5443 132.465 81.501 132.373 81.4143C132.283 81.3277 132.238 81.2235 132.238 81.1019C132.238 80.9802 132.283 80.876 132.373 80.7894C132.465 80.7027 132.575 80.6594 132.702 80.6594C132.829 80.6594 132.938 80.7027 133.029 80.7894C133.121 80.876 133.167 80.9802 133.167 81.1019C133.167 81.2235 133.121 81.3277 133.029 81.4143C132.938 81.501 132.829 81.5443 132.702 81.5443ZM137.227 83.2036L136.641 83.3695C136.604 83.2718 136.55 83.1769 136.478 83.0847C136.408 82.9907 136.312 82.9132 136.19 82.8524C136.068 82.7915 135.913 82.7611 135.723 82.7611C135.463 82.7611 135.246 82.821 135.073 82.9409C134.901 83.0589 134.816 83.2091 134.816 83.3916C134.816 83.5539 134.875 83.682 134.993 83.776C135.111 83.8701 135.295 83.9484 135.546 84.0111L136.176 84.166C136.556 84.2581 136.839 84.3992 137.025 84.5891C137.212 84.7771 137.305 85.0196 137.305 85.3164C137.305 85.5597 137.235 85.7773 137.094 85.969C136.956 86.1608 136.763 86.3119 136.514 86.4226C136.265 86.5332 135.975 86.5885 135.645 86.5885C135.212 86.5885 134.854 86.4945 134.57 86.3064C134.286 86.1184 134.106 85.8437 134.03 85.4823L134.65 85.3275C134.709 85.5561 134.82 85.7275 134.984 85.8418C135.15 85.9561 135.367 86.0133 135.634 86.0133C135.938 86.0133 136.18 85.9488 136.359 85.8197C136.54 85.6888 136.63 85.5321 136.63 85.3496C136.63 85.2021 136.578 85.0786 136.475 84.979C136.372 84.8776 136.213 84.802 135.999 84.7522L135.291 84.5863C134.902 84.4941 134.617 84.351Line truncated
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="628" height="340" stroke="#071517"/>
|
||||
<defs>
|
||||
<filter id="filter0_f_4769_1899" x="-439.266" y="-46.4264" width="971.703" height="782.895" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="127.022" result="effect1_foregroundBlur_4769_1899"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_4769_1899" x="-14.8287" y="-172.7" width="971.703" height="782.895" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="127.022" result="effect1_foregroundBlur_4769_1899"/>
|
||||
</filter>
|
||||
<filter id="filter2_b_4769_1899" x="21.6437" y="37.7374" width="665.877" height="338.646" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="17.0655"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_4769_1899"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_4769_1899" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter3_b_4769_1899" x="22.0331" y="38.1268" width="99.1838" height="341.761" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="17.0655"/>
|
||||
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_4769_1899"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_4769_1899" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_4769_1899" x1="46.5853" y1="207.617" x2="46.5853" y2="482.425" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#13A470"/>
|
||||
<stop offset="1" stop-color="#6F13A4" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4769_1899" x1="471.023" y1="81.3438" x2="471.023" y2="356.152" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#13A470"/>
|
||||
<stop offset="1" stop-color="#6F13A4" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4769_1899" x1="154.088" y1="64.8163" x2="199.654" y2="259.033" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.23"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4769_1899">
|
||||
<rect x="1" y="1" width="627" height="339" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,15 @@
|
||||
<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
|
||||
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#249361"/>
|
||||
<stop offset="1" stop-color="#3ECF8E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -490,6 +490,15 @@ launch week
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes transformSpin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* recharts */
|
||||
|
||||
.recharts-surface {
|
||||
|
||||
@@ -25,11 +25,16 @@ module.exports = config({
|
||||
spin: {
|
||||
'0%, 100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
marquee: {
|
||||
'0%': { transform: 'translateX(0%)' },
|
||||
'100%': { transform: 'translateX(-100%)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'flash-code': 'flash-code 1s forwards',
|
||||
'flash-code-slow': 'flash-code 2s forwards',
|
||||
spin: 'spin 1s both infinite',
|
||||
marquee: 'marquee 35s linear infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -31,10 +31,10 @@ Jos haluat nähdä, miten osallistuminen tapahtuu, käy osoitteessa [Getting Sta
|
||||
|
||||
## Yhteisö ja tuki
|
||||
|
||||
- [Yhteisön foorumi](https://github.com/supabase/supabase/discussions). Paras: Apua rakentamiseen, keskustelua tietokannan parhaista käytännöistä.
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues). Parasta: Supabasea käytettäessä kohdatut viat ja virheet.
|
||||
- [Sähköpostituki](https://supabase.com/docs/support#business-support). Paras: tietokantaan tai infrastruktuuriin liittyvät ongelmat.
|
||||
- [Discord](https://discord.supabase.com). Paras: sovellusten jakamiseen ja yhteisön kanssa hengailuun.
|
||||
- [Yhteisön foorumi](https://github.com/supabase/supabase/discussions). Sopii parhaiten: Apua rakentamiseen, keskustelua tietokannan parhaista käytännöistä.
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues). Sopii parhaiten: Supabasea käytettäessä kohdatut viat ja virheet.
|
||||
- [Sähköpostituki](https://supabase.com/docs/support#business-support). Sopii parhaiten: Tietokantaan tai infrastruktuuriin liittyvät ongelmat.
|
||||
- [Discord](https://discord.supabase.com). Sopii parhaiten: Sovellusten jakamiseen ja yhteisön kanssa hengailuun.
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"rehype-slug": "^5.1.0",
|
||||
"remark": "^14.0.2",
|
||||
"remark-admonitions": "^1.2.1",
|
||||
"remark-emoji": "^3.1.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-slug": "^7.0.1",
|
||||
"shiki": "^0.11.1",
|
||||
@@ -20160,6 +20161,15 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/emoticon": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.0.1.tgz",
|
||||
"integrity": "sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
@@ -29753,6 +29763,14 @@
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-emoji": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
|
||||
"integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.11",
|
||||
"license": "MIT",
|
||||
@@ -34592,6 +34610,19 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-emoji": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-3.1.2.tgz",
|
||||
"integrity": "sha512-QwhAzNk27Ol64uV4z/3n55MKrNz9bhr8wg+mO5aGqIYDS+jUarS1d8Y0ZIeEBVhfGkXj6gGYM+727sOgAPvV/A==",
|
||||
"dependencies": {
|
||||
"emoticon": "^4.0.1",
|
||||
"mdast-util-find-and-replace": "^2.2.2",
|
||||
"node-emoji": "^1.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-external-links": {
|
||||
"version": "8.0.0",
|
||||
"dev": true,
|
||||
|
||||
@@ -51,7 +51,7 @@ export function Default() {
|
||||
)
|
||||
}
|
||||
|
||||
export function withSeperators() {
|
||||
export function withSeparators() {
|
||||
return (
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
|
||||
@@ -3,9 +3,10 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@ui/lib/utils'
|
||||
|
||||
// Ivan: replaced [&:has(svg)]:pl-14 with [&>svg~*]:pl-10 cause of github.com/shadcn-ui/ui/issues/998
|
||||
const alertVariants = cva(
|
||||
cn(
|
||||
'relative w-full rounded-lg border p-4 [&:has(svg)]:pl-14 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
'relative w-full text-sm rounded-lg border p-4 [&>svg~*]:pl-10 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
'[&>svg]:w-[23px] [&>svg]:h-[23px] [&>svg]:p-1 [&>svg]:flex [&>svg]:rounded'
|
||||
),
|
||||
{
|
||||
@@ -16,7 +17,7 @@ const alertVariants = cva(
|
||||
destructive:
|
||||
'text border-destructive-400 bg-destructive-200 [&>svg]:text-destructive-200 [&>svg]:bg-destructive-600',
|
||||
warning:
|
||||
'text-warning-100 dark:text-warning border-warning-400 bg-warning-200 [&>svg]:text-warning-200 [&>svg]:bg-warning-600',
|
||||
'border-warning-400 bg-warning-200 [&>svg]:text-warning-200 [&>svg]:bg-warning-600',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -79,7 +79,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-overlay-hover focus:text-strong data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-overlay-hover focus:text-strong data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
clispec: '001'
|
||||
info:
|
||||
id: cli
|
||||
version: 1.77.9
|
||||
version: 1.82.2
|
||||
title: Supabase CLI
|
||||
language: sh
|
||||
source: https://github.com/supabase/cli
|
||||
@@ -1388,45 +1388,268 @@ commands:
|
||||
default_value: ''
|
||||
- id: supabase-inspect
|
||||
title: supabase inspect
|
||||
summary: Tools to inspect your Supabase Database
|
||||
summary: Tools to inspect your Supabase project
|
||||
tags:
|
||||
- local-dev
|
||||
links: []
|
||||
subcommands:
|
||||
- supabase-inspect-db:cache-hit
|
||||
- supabase-inspect-db:index-usage
|
||||
- supabase-inspect-db:replication-slots
|
||||
- supabase-inspect-db
|
||||
flags: []
|
||||
- id: supabase-inspect-db:replication-slots
|
||||
title: supabase inspect db:replication-slots
|
||||
summary: Shows information about replication slots on the database
|
||||
- id: supabase-inspect-db
|
||||
title: supabase inspect db
|
||||
summary: Tools to inspect your Supabase database
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db:replication-slots
|
||||
subcommands:
|
||||
- supabase-inspect-db-bloat
|
||||
- supabase-inspect-db-blocking
|
||||
- supabase-inspect-db-cache-hit
|
||||
- supabase-inspect-db-calls
|
||||
- supabase-inspect-db-index-sizes
|
||||
- supabase-inspect-db-index-usage
|
||||
- supabase-inspect-db-locks
|
||||
- supabase-inspect-db-long-running-queries
|
||||
- supabase-inspect-db-outliers
|
||||
- supabase-inspect-db-replication-slots
|
||||
- supabase-inspect-db-role-connections
|
||||
- supabase-inspect-db-seq-scans
|
||||
- supabase-inspect-db-table-index-sizes
|
||||
- supabase-inspect-db-table-record-counts
|
||||
- supabase-inspect-db-table-sizes
|
||||
- supabase-inspect-db-total-index-size
|
||||
- supabase-inspect-db-total-table-sizes
|
||||
- supabase-inspect-db-unused-indexes
|
||||
- supabase-inspect-db-vacuum-stats
|
||||
flags: []
|
||||
- id: supabase-inspect-db-vacuum-stats
|
||||
title: supabase inspect db vacuum-stats
|
||||
summary: Show statistics related to vacuum operations per table
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db vacuum-stats
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db:index-usage
|
||||
title: supabase inspect db:index-usage
|
||||
summary: Shows information about the efficiency of indexes
|
||||
- id: supabase-inspect-db-unused-indexes
|
||||
title: supabase inspect db unused-indexes
|
||||
summary: Show indexes with low usage
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db:index-usage
|
||||
usage: supabase inspect db unused-indexes
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db:cache-hit
|
||||
title: supabase inspect db:cache-hit
|
||||
summary: Shows cache hit rates for tables and indices
|
||||
- id: supabase-inspect-db-total-table-sizes
|
||||
title: supabase inspect db total-table-sizes
|
||||
summary: Show total table sizes, including table index sizes
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db:cache-hit
|
||||
usage: supabase inspect db total-table-sizes
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-total-index-size
|
||||
title: supabase inspect db total-index-size
|
||||
summary: Show total size of all indexes
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db total-index-size
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-table-sizes
|
||||
title: supabase inspect db table-sizes
|
||||
summary: |
|
||||
Show table sizes of individual tables without their index sizes
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db table-sizes
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-table-record-counts
|
||||
title: supabase inspect db table-record-counts
|
||||
summary: Show estimated number of rows per table
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db table-record-counts
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-table-index-sizes
|
||||
title: supabase inspect db table-index-sizes
|
||||
summary: Show index sizes of individual tables
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db table-index-sizes
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-seq-scans
|
||||
title: supabase inspect db seq-scans
|
||||
summary: Show number of sequential scans recorded against all tables
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db seq-scans
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-role-connections
|
||||
title: supabase inspect db role-connections
|
||||
summary: Show number of active connections for all database roles
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db role-connections
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-replication-slots
|
||||
title: supabase inspect db replication-slots
|
||||
summary: Show information about replication slots on the database
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db replication-slots
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-outliers
|
||||
title: supabase inspect db outliers
|
||||
summary: |
|
||||
Show queries from pg_stat_statements ordered by total execution time
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db outliers
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-long-running-queries
|
||||
title: supabase inspect db long-running-queries
|
||||
summary: |
|
||||
Show currently running queries running for longer than 5 minutes
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db long-running-queries
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-locks
|
||||
title: supabase inspect db locks
|
||||
summary: |
|
||||
Show queries which have taken out an exclusive lock on a relation
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db locks
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-index-usage
|
||||
title: supabase inspect db index-usage
|
||||
summary: Show information about the efficiency of indexes
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db index-usage
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-index-sizes
|
||||
title: supabase inspect db index-sizes
|
||||
summary: Show index sizes of individual indexes
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db index-sizes
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-calls
|
||||
title: supabase inspect db calls
|
||||
summary: |
|
||||
Show queries from pg_stat_statements ordered by total times called
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db calls
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-cache-hit
|
||||
title: supabase inspect db cache-hit
|
||||
summary: Show cache hit rates for tables and indices
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db cache-hit
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-blocking
|
||||
title: supabase inspect db blocking
|
||||
summary: |
|
||||
Show queries that are holding locks and the queries that are waiting for them to be released
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db blocking
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
name: --db-url <string>
|
||||
description: connect using the specified database url
|
||||
default_value: ''
|
||||
- id: supabase-inspect-db-bloat
|
||||
title: supabase inspect db bloat
|
||||
summary: |
|
||||
Estimates space allocated to a relation that is full of dead tuples
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase inspect db bloat
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: db-url
|
||||
@@ -1908,6 +2131,14 @@ commands:
|
||||
description: |
|
||||
Print the migrations that would be applied, but don't actually apply them.
|
||||
default_value: 'false'
|
||||
- id: include-roles
|
||||
name: --include-roles
|
||||
description: Include custom roles from supabase/roles.sql.
|
||||
default_value: 'false'
|
||||
- id: include-seed
|
||||
name: --include-seed
|
||||
description: Include seed data from supabase/seed.sql.
|
||||
default_value: 'false'
|
||||
- id: password
|
||||
name: -p, --password <string>
|
||||
description: Password to your remote Postgres database.
|
||||
@@ -1990,7 +2221,7 @@ commands:
|
||||
|
||||
Requires your local project to be linked to a remote database by running `supabase link`. For self-hosted databases, you can pass in the connection parameters using `--db-url` flag.
|
||||
|
||||
Runs `pg_dump` in a container with additional flags to exclude Supabase managed schemas. The ignored schemas include auth, stroage, and those created by extensions.
|
||||
Runs `pg_dump` in a container with additional flags to exclude Supabase managed schemas. The ignored schemas include auth, storage, and those created by extensions.
|
||||
|
||||
The default dump does not contain any data or custom roles. To dump those contents explicitly, specify either the `--data-only` and `--role-only` flag.
|
||||
examples:
|
||||
@@ -2021,6 +2252,10 @@ commands:
|
||||
name: --data-only
|
||||
description: Dumps only data records.
|
||||
default_value: 'false'
|
||||
- id: dry-run
|
||||
name: --dry-run
|
||||
description: Print the pg_dump script that would be executed.
|
||||
default_value: 'false'
|
||||
- id: file
|
||||
name: -f, --file <string>
|
||||
description: File path to save the dumped contents.
|
||||
@@ -2037,6 +2272,10 @@ commands:
|
||||
name: --role-only
|
||||
description: Dumps only cluster roles.
|
||||
default_value: 'false'
|
||||
- id: schema
|
||||
name: -s, --schema <strings>
|
||||
description: List of schema to include.
|
||||
default_value: all
|
||||
- id: use-copy
|
||||
name: --use-copy
|
||||
description: Uses copy statements in place of inserts.
|
||||
@@ -2258,3 +2497,210 @@ commands:
|
||||
name: --no-descriptions
|
||||
description: disable completion descriptions
|
||||
default_value: 'false'
|
||||
- id: supabase-branches
|
||||
title: supabase branches
|
||||
summary: Manage Supabase preview branches
|
||||
tags:
|
||||
- management-api
|
||||
links: []
|
||||
subcommands:
|
||||
- supabase-branches-create
|
||||
- supabase-branches-delete
|
||||
- supabase-branches-disable
|
||||
- supabase-branches-get
|
||||
- supabase-branches-list
|
||||
- supabase-branches-update
|
||||
flags: []
|
||||
- id: supabase-branches-update
|
||||
title: supabase branches update
|
||||
summary: Update a preview branch
|
||||
description: Update a preview branch by its ID.
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase branches update <branch-id> [flags]
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: git-branch
|
||||
name: --git-branch <string>
|
||||
description: Change the associated git branch.
|
||||
default_value: ''
|
||||
- id: name
|
||||
name: --name <string>
|
||||
description: Rename the preview branch.
|
||||
default_value: ''
|
||||
- id: experimental
|
||||
name: --experimental
|
||||
description: enable experimental features
|
||||
required: true
|
||||
default_value: 'false'
|
||||
- id: supabase-branches-list
|
||||
title: supabase branches list
|
||||
summary: List all preview branches
|
||||
description: List all preview branches of the linked project.
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase branches list
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: experimental
|
||||
name: --experimental
|
||||
description: enable experimental features
|
||||
required: true
|
||||
default_value: 'false'
|
||||
- id: supabase-branches-get
|
||||
title: supabase branches get
|
||||
summary: Retrieve details of a preview branch
|
||||
description: Retrieve details of the specified preview branch.
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase branches get <branch-id>
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: experimental
|
||||
name: --experimental
|
||||
description: enable experimental features
|
||||
required: true
|
||||
default_value: 'false'
|
||||
- id: supabase-branches-disable
|
||||
title: supabase branches disable
|
||||
summary: Disable preview branching
|
||||
description: Disable preview branching for the linked project.
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase branches disable
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: experimental
|
||||
name: --experimental
|
||||
description: enable experimental features
|
||||
required: true
|
||||
default_value: 'false'
|
||||
- id: supabase-branches-delete
|
||||
title: supabase branches delete
|
||||
summary: Delete a preview branch
|
||||
description: Delete a preview branch by its ID.
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase branches delete <branch-id>
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: experimental
|
||||
name: --experimental
|
||||
description: enable experimental features
|
||||
required: true
|
||||
default_value: 'false'
|
||||
- id: supabase-branches-create
|
||||
title: supabase branches create
|
||||
summary: Create a preview branch
|
||||
description: Create a preview branch for the linked project.
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase branches create <name> [flags]
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: region
|
||||
name: --region <string>
|
||||
description: Select a region to deploy the branch database.
|
||||
default_value: ''
|
||||
accepted_values:
|
||||
- id: ams
|
||||
name: ams
|
||||
type: string
|
||||
- id: arn
|
||||
name: arn
|
||||
type: string
|
||||
- id: bog
|
||||
name: bog
|
||||
type: string
|
||||
- id: bos
|
||||
name: bos
|
||||
type: string
|
||||
- id: cdg
|
||||
name: cdg
|
||||
type: string
|
||||
- id: den
|
||||
name: den
|
||||
type: string
|
||||
- id: dfw
|
||||
name: dfw
|
||||
type: string
|
||||
- id: ewr
|
||||
name: ewr
|
||||
type: string
|
||||
- id: fra
|
||||
name: fra
|
||||
type: string
|
||||
- id: gdl
|
||||
name: gdl
|
||||
type: string
|
||||
- id: gig
|
||||
name: gig
|
||||
type: string
|
||||
- id: gru
|
||||
name: gru
|
||||
type: string
|
||||
- id: hkg
|
||||
name: hkg
|
||||
type: string
|
||||
- id: iad
|
||||
name: iad
|
||||
type: string
|
||||
- id: jnb
|
||||
name: jnb
|
||||
type: string
|
||||
- id: lax
|
||||
name: lax
|
||||
type: string
|
||||
- id: lhr
|
||||
name: lhr
|
||||
type: string
|
||||
- id: maa
|
||||
name: maa
|
||||
type: string
|
||||
- id: mad
|
||||
name: mad
|
||||
type: string
|
||||
- id: mia
|
||||
name: mia
|
||||
type: string
|
||||
- id: nrt
|
||||
name: nrt
|
||||
type: string
|
||||
- id: ord
|
||||
name: ord
|
||||
type: string
|
||||
- id: otp
|
||||
name: otp
|
||||
type: string
|
||||
- id: qro
|
||||
name: qro
|
||||
type: string
|
||||
- id: scl
|
||||
name: scl
|
||||
type: string
|
||||
- id: sea
|
||||
name: sea
|
||||
type: string
|
||||
- id: sin
|
||||
name: sin
|
||||
type: string
|
||||
- id: sjc
|
||||
name: sjc
|
||||
type: string
|
||||
- id: syd
|
||||
name: syd
|
||||
type: string
|
||||
- id: waw
|
||||
name: waw
|
||||
type: string
|
||||
- id: yul
|
||||
name: yul
|
||||
type: string
|
||||
- id: yyz
|
||||
name: yyz
|
||||
type: string
|
||||
- id: experimental
|
||||
name: --experimental
|
||||
description: enable experimental features
|
||||
required: true
|
||||
default_value: 'false'
|
||||
@@ -210,28 +210,126 @@
|
||||
"title": "Inspect",
|
||||
"items": [
|
||||
{
|
||||
"id": "supabase-inspect",
|
||||
"id": "supabase-inspect-db",
|
||||
"title": "Inspect database statistics",
|
||||
"slug": "supabase-inspect",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db:cache-hit",
|
||||
"title": "Show cache hit rates for tables and indexes",
|
||||
"slug": "supabase-inspect-db:cache-hit",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db:index-usage",
|
||||
"title": "Show usage efficiency of indexes",
|
||||
"slug": "supabase-inspect-db:index-usage",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db:replication-slots",
|
||||
"title": "Show information about replication slots",
|
||||
"slug": "supabase-inspect-db:replication-slots",
|
||||
"type": "cli-command"
|
||||
"slug": "supabase-inspect-db",
|
||||
"type": "cli-command",
|
||||
"items": [
|
||||
{
|
||||
"id": "supabase-inspect-db-calls",
|
||||
"title": "Show most frequently run queries",
|
||||
"slug": "supabase-inspect-db-calls",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-long-running-queries",
|
||||
"title": "Show long running queries",
|
||||
"slug": "supabase-inspect-db-long-running-queries",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-outliers",
|
||||
"title": "Show queries ordered by total execution time",
|
||||
"slug": "supabase-inspect-db-outliers",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-blocking",
|
||||
"title": "Show queries waiting and holding locks",
|
||||
"slug": "supabase-inspect-db-blocking",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-locks",
|
||||
"title": "Show queries taking exclusive locks",
|
||||
"slug": "supabase-inspect-db-locks",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-total-index-size",
|
||||
"title": "Show total size of all indexes",
|
||||
"slug": "supabase-inspect-db-total-index-size",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-index-sizes",
|
||||
"title": "Show sizes of individual indexes",
|
||||
"slug": "supabase-inspect-db-index-sizes",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-index-usage",
|
||||
"title": "Show information about index efficiency",
|
||||
"slug": "supabase-inspect-db-index-usage",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-unused-indexes",
|
||||
"title": "Show indexes with low usage",
|
||||
"slug": "supabase-inspect-db-unused-indexes",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-total-table-sizes",
|
||||
"title": "Show total size of all tables",
|
||||
"slug": "supabase-inspect-db-total-table-sizes",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-table-sizes",
|
||||
"title": "Show sizes of individual tables",
|
||||
"slug": "supabase-inspect-db-table-sizes",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-table-index-sizes",
|
||||
"title": "Show index sizes of individual tables",
|
||||
"slug": "supabase-inspect-db-table-index-sizes",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-cache-hit",
|
||||
"title": "Show cache hit rates for tables and indices",
|
||||
"slug": "supabase-inspect-db-cache-hit",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-table-record-counts",
|
||||
"title": "Show estimated number of rows per table",
|
||||
"slug": "supabase-inspect-db-table-record-counts",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-seq-scans",
|
||||
"title": "Show number of sequential scans for all tables",
|
||||
"slug": "supabase-inspect-db-seq-scans",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-replication-slots",
|
||||
"title": "Show information about replication slots",
|
||||
"slug": "supabase-inspect-db-replication-slots",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-role-connections",
|
||||
"title": "Show number of active connections",
|
||||
"slug": "supabase-inspect-db-role-connections",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-bloat",
|
||||
"title": "Show estimated database bloat",
|
||||
"slug": "supabase-inspect-db-bloat",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-inspect-db-vacuum-stats",
|
||||
"title": "Show statistics related to vacuum operations",
|
||||
"slug": "supabase-inspect-db-vacuum-stats",
|
||||
"type": "cli-command"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -259,12 +357,60 @@
|
||||
},
|
||||
{
|
||||
"id": "supabase-projects-api-keys",
|
||||
"title": "Show API keys for a Supabase project",
|
||||
"title": "Show API keys for a project",
|
||||
"slug": "supabase-projects-api-keys",
|
||||
"type": "cli-command"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "category",
|
||||
"title": "Branches",
|
||||
"items": [
|
||||
{
|
||||
"id": "supabase-branches",
|
||||
"title": "Manage preview branches",
|
||||
"slug": "supabase-branches",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-branches-create",
|
||||
"title": "Create a preview branch",
|
||||
"slug": "supabase-branches-create",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-branches-list",
|
||||
"title": "List all preview branches",
|
||||
"slug": "supabase-branches-list",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-branches-get",
|
||||
"title": "Show connection details for a preview branch",
|
||||
"slug": "supabase-branches-get",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-branches-update",
|
||||
"title": "Update a preview branch",
|
||||
"slug": "supabase-branches-update",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-branches-delete",
|
||||
"title": "Delete a preview branch",
|
||||
"slug": "supabase-branches-delete",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-branches-disable",
|
||||
"title": "Disable preview branching",
|
||||
"slug": "supabase-branches-disable",
|
||||
"type": "cli-command"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "category",
|
||||
"title": "Organizations",
|
||||
@@ -275,6 +421,12 @@
|
||||
"slug": "supabase-orgs",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-orgs-create",
|
||||
"title": "Create an organization",
|
||||
"slug": "supabase-orgs-create",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-orgs-list",
|
||||
"title": "List all organizations",
|
||||
|
||||
@@ -66,7 +66,7 @@ functions:
|
||||
Or open OTP links directly in your app.
|
||||
|
||||
### PKCE Authentication flow
|
||||
suapbase-kt supports the [PKCE authentication flow](https://supabase.com/blog/supabase-auth-sso-pkce).
|
||||
supabase-kt supports the [PKCE authentication flow](https://supabase.com/blog/supabase-auth-sso-pkce).
|
||||
To use it, you just have to change the `flowType` in the GoTrue configuration:
|
||||
```kotlin
|
||||
install(GoTrue) {
|
||||
|
||||
@@ -2,12 +2,12 @@ import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import useLatest from 'hooks/misc/useLatest'
|
||||
import { useParams } from 'common'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { DEFAULT_HOME, LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
|
||||
// Ideally these could all be within a _middleware when we use Next 12
|
||||
const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
@@ -51,7 +51,7 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
|
||||
if (!isValidOrg) {
|
||||
ui.setNotification({ category: 'error', message: 'This organization does not exist' })
|
||||
router.push(navLayoutV2 ? `/org/${organizations[0].slug}` : '/projects')
|
||||
router.push(navLayoutV2 ? `/org/${organizations[0].slug}` : DEFAULT_HOME)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -68,10 +68,11 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
// Check validity of project that the user is trying to access
|
||||
const projects = projectsRef.current ?? []
|
||||
const isValidProject = projects.some((project) => project.ref === ref)
|
||||
const isValidBranch = projects.some((project) => project.preview_branch_refs.includes(ref))
|
||||
|
||||
if (!isValidProject) {
|
||||
if (!isValidProject && !isValidBranch) {
|
||||
ui.setNotification({ category: 'error', message: 'This project does not exist' })
|
||||
router.push(navLayoutV2 ? `/org/${organizations?.[0].slug}` : '/projects')
|
||||
router.push(navLayoutV2 ? `/org/${organizations?.[0].slug}` : DEFAULT_HOME)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { Form, Input } from 'ui'
|
||||
import { boolean, number, object, string } from 'yup'
|
||||
|
||||
@@ -10,17 +10,14 @@ import {
|
||||
FormPanel,
|
||||
FormSection,
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from 'components/ui/Forms'
|
||||
import { useCheckPermissions, useStore } from 'hooks'
|
||||
import { useCheckPermissions, useSelectedProject, useStore } from 'hooks'
|
||||
|
||||
const SiteUrl = observer(() => {
|
||||
const { authConfig, ui } = useStore()
|
||||
const { isLoaded } = authConfig
|
||||
|
||||
const formId = 'auth-config-general-form'
|
||||
const [hidden, setHidden] = useState(true)
|
||||
|
||||
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
|
||||
|
||||
const INITIAL_VALUES = {
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
Form,
|
||||
IconAlertTriangle,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
Input,
|
||||
InputNumber,
|
||||
Toggle,
|
||||
} from 'ui'
|
||||
import { number, object, string } from 'yup'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Alert, Button, Form, Input, InputNumber, Toggle, IconEye, IconEyeOff } from 'ui'
|
||||
|
||||
import {
|
||||
FormActions,
|
||||
@@ -12,7 +25,7 @@ import {
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from 'components/ui/Forms'
|
||||
import { useStore, useCheckPermissions } from 'hooks'
|
||||
import { useCheckPermissions, useStore } from 'hooks'
|
||||
import { urlRegex } from './../Auth.constants'
|
||||
import { defaultDisabledSmtpFormValues } from './SmtpForm.constants'
|
||||
import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils'
|
||||
@@ -180,11 +193,38 @@ const SmtpForm = () => {
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
|
||||
{enableSmtp && !isValidSmtpConfig && (
|
||||
{enableSmtp ? (
|
||||
!isValidSmtpConfig && (
|
||||
<div className="mx-8 mb-8 -mt-4">
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>All fields below must be filled</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
The following fields must be filled before custom SMTP can be properly
|
||||
enabled
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="mx-8 mb-8 -mt-4">
|
||||
<Alert withIcon variant="warning" title="All fields below must be filled">
|
||||
The following fields must be filled before custom SMTP can be properly enabled
|
||||
</Alert>
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>Built-in email service is rate-limited!</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
You're using the built-in email service. The service has rate limits and it's
|
||||
not meant to be used for production apps. Check the{' '}
|
||||
<a
|
||||
href="https://supabase.com/docs/guides/platform/going-into-prod#auth-rate-limits"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
for an up-to-date information on the current rate limits. Please use a custom
|
||||
SMTP server if you're planning on having large number of users.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import { SUBSCRIPTION_PANEL_KEYS, useSubscriptionPageStateSnapshot } from 'state
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export interface SubscriptionProps {}
|
||||
export interface SubscriptionProps { }
|
||||
|
||||
const Subscription = ({}: SubscriptionProps) => {
|
||||
const Subscription = ({ }: SubscriptionProps) => {
|
||||
const router = useRouter()
|
||||
const organization = useSelectedOrganization()
|
||||
const snap = useSubscriptionPageStateSnapshot()
|
||||
@@ -24,32 +24,6 @@ const Subscription = ({}: SubscriptionProps) => {
|
||||
const panel = router.query.panel
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
|
||||
useEffect(() => {
|
||||
if (isOrgBilling) {
|
||||
const { ref, panel } = router.query
|
||||
let redirectUri = `/org/${organization.slug}/billing`
|
||||
switch (panel) {
|
||||
case 'subscriptionPlan':
|
||||
redirectUri = `/org/${organization.slug}/billing?panel=subscriptionPlan`
|
||||
break
|
||||
case 'costControl':
|
||||
redirectUri = `/org/${organization.slug}/billing?panel=costControl`
|
||||
break
|
||||
case 'computeInstance':
|
||||
redirectUri = `/project/${ref}/settings/addons?panel=computeInstance`
|
||||
break
|
||||
case 'pitr':
|
||||
redirectUri = `/project/${ref}/settings/addons?panel=pitr`
|
||||
break
|
||||
case 'customDomain':
|
||||
redirectUri = `/project/${ref}/settings/addons?panel=customDomain`
|
||||
break
|
||||
}
|
||||
|
||||
router.push(redirectUri)
|
||||
}
|
||||
}, [router, organization?.slug, isOrgBilling])
|
||||
|
||||
useEffect(() => {
|
||||
if (panel && typeof panel === 'string' && allowedValues.includes(panel)) {
|
||||
snap.setPanelKey(panel as SUBSCRIPTION_PANEL_KEYS)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { partition } from 'lodash'
|
||||
import { useState } from 'react'
|
||||
import { Button, IconSearch, Input } from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useBranchDeleteMutation } from 'data/branches/branch-delete-mutation'
|
||||
import { Branch, useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useSelectedProject, useStore } from 'hooks'
|
||||
import { MainBranchPanel } from './BranchPanels'
|
||||
import CreateBranchSidePanel from './CreateBranchSidePanel'
|
||||
import PreviewBranches from './PreviewBranches'
|
||||
import PullRequests from './PullRequests'
|
||||
import UpdateBranchSidePanel from './UpdateBranchSidePanel'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const BranchManagement = () => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const projectDetails = useSelectedProject()
|
||||
|
||||
const isBranch = projectDetails?.parent_project_ref !== undefined
|
||||
const projectRef =
|
||||
projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
|
||||
|
||||
const [showCreateBranch, setShowCreateBranch] = useState(false)
|
||||
const [selectedBranchToUpdate, setSelectedBranchToUpdate] = useState<Branch>()
|
||||
const [selectedBranchToDelete, setSelectedBranchToDelete] = useState<Branch>()
|
||||
|
||||
const { data: branches, error, isLoading, isError, isSuccess } = useBranchesQuery({ projectRef })
|
||||
const [[mainBranch], previewBranches] = partition(branches, (branch) => branch.is_default)
|
||||
|
||||
const { mutate: deleteBranch, isLoading: isDeleting } = useBranchDeleteMutation({
|
||||
onSuccess: () => {
|
||||
if (selectedBranchToDelete?.project_ref === ref) {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message:
|
||||
'Successfully deleted branch. You are now currently on the main branch of your project.',
|
||||
})
|
||||
router.push(`/project/${projectRef}/branches`)
|
||||
} else {
|
||||
ui.setNotification({ category: 'success', message: 'Successfully deleted branch' })
|
||||
}
|
||||
setSelectedBranchToDelete(undefined)
|
||||
},
|
||||
})
|
||||
|
||||
const onConfirmDeleteBranch = () => {
|
||||
if (selectedBranchToDelete == undefined) return console.error('No branch selected')
|
||||
if (projectRef == undefined) return console.error('Project ref is required')
|
||||
deleteBranch({ id: selectedBranchToDelete?.id, projectRef })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<div className="col-span-12">
|
||||
<h3 className="text-xl mb-8">Branch Manager</h3>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input placeholder="Search branch" size="small" icon={<IconSearch />} />
|
||||
</div>
|
||||
<Button onClick={() => setShowCreateBranch(true)}>Create preview branch</Button>
|
||||
</div>
|
||||
<div className="">
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve branches" />}
|
||||
{isSuccess && (
|
||||
<>
|
||||
<MainBranchPanel branch={mainBranch} onSelectUpdate={() => {}} />
|
||||
<PullRequests previewBranches={previewBranches} />
|
||||
<PreviewBranches
|
||||
previewBranches={previewBranches}
|
||||
onSelectUpdateBranch={setSelectedBranchToUpdate}
|
||||
onSelectDeleteBranch={setSelectedBranchToDelete}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
|
||||
<TextConfirmModal
|
||||
size="medium"
|
||||
visible={selectedBranchToDelete !== undefined}
|
||||
onCancel={() => setSelectedBranchToDelete(undefined)}
|
||||
onConfirm={() => onConfirmDeleteBranch()}
|
||||
title="Delete branch"
|
||||
loading={isDeleting}
|
||||
confirmLabel={`Delete branch`}
|
||||
confirmPlaceholder="Type in name of branch"
|
||||
confirmString={selectedBranchToDelete?.name ?? ''}
|
||||
text={`This will delete your branch "${selectedBranchToDelete?.name}"`}
|
||||
alert="You cannot recover this branch once it is deleted!"
|
||||
/>
|
||||
|
||||
<CreateBranchSidePanel
|
||||
visible={showCreateBranch}
|
||||
onClose={() => setShowCreateBranch(false)}
|
||||
/>
|
||||
|
||||
<UpdateBranchSidePanel
|
||||
selectedBranch={selectedBranchToUpdate}
|
||||
onClose={() => setSelectedBranchToUpdate(undefined)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BranchManagement
|
||||
@@ -0,0 +1,160 @@
|
||||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
import { noop } from 'lodash'
|
||||
import { PropsWithChildren, forwardRef, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenuContent_Shadcn_,
|
||||
DropdownMenuItem_Shadcn_,
|
||||
DropdownMenuTrigger_Shadcn_,
|
||||
DropdownMenu_Shadcn_,
|
||||
IconEdit,
|
||||
IconGitBranch,
|
||||
IconGitHub,
|
||||
IconMoreVertical,
|
||||
IconShield,
|
||||
IconTrash,
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
|
||||
interface BranchPanelProps {
|
||||
branch: Branch
|
||||
onSelectUpdate?: () => void
|
||||
onSelectDelete?: () => void
|
||||
}
|
||||
|
||||
const MainBranchPanel = ({ branch, onSelectUpdate = noop }: BranchPanelProps) => {
|
||||
const { ref } = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const isActive = ref === branch.project_ref
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg">
|
||||
<div className="bg-surface-200 shadow-sm flex justify-between items-center pl-8 pr-6 py-3 rounded-t-lg text-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-8 h-8 bg-scale-200 rounded-md flex items-center justify-center">
|
||||
<IconGitHub size={18} strokeWidth={2} />
|
||||
</div>
|
||||
<p>Github branch workflow</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-surface-100 border-t shadow-sm flex justify-between items-center pl-8 pr-6 py-3 rounded-b-lg text-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-8 h-8 bg-amber-300 rounded-md flex items-center justify-center">
|
||||
<IconShield size={18} strokeWidth={2} className="text-amber-900" />
|
||||
</div>
|
||||
<p>{branch.name}</p>
|
||||
<Badge color="amber">Production</Badge>
|
||||
{isActive && <Badge color="green">Selected</Badge>}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<DropdownMenu_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
||||
<DropdownMenuTrigger_Shadcn_>
|
||||
<Button asChild type="text" className="px-1" icon={<IconMoreVertical size={14} />}>
|
||||
<span></span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger_Shadcn_>
|
||||
<DropdownMenuContent_Shadcn_ side="bottom" align="end">
|
||||
<DropdownMenuItem_Shadcn_ className="flex gap-2" onSelect={() => onSelectUpdate()}>
|
||||
<IconEdit size={14} />
|
||||
Edit branch
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</DropdownMenuContent_Shadcn_>
|
||||
</DropdownMenu_Shadcn_>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BranchContainer = ({ className, children }: PropsWithChildren<{ className?: string }>) => {
|
||||
return (
|
||||
<div className="list-none ml-6 pl-8 pb-4 border-l border-scale-600 dark:border-scale-400 relative last:!border-transparent">
|
||||
<div className="absolute w-[33px] rounded-bl-full border-b border-l border-scale-600 dark:border-scale-400 h-10 -left-px" />
|
||||
<div
|
||||
className={clsx(
|
||||
'border shadow-sm flex justify-between items-center pl-8 pr-6 py-4 rounded-lg text-sm',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BranchPanel = ({
|
||||
branch,
|
||||
onSelectUpdate = noop,
|
||||
onSelectDelete = noop,
|
||||
}: BranchPanelProps) => {
|
||||
const { ref } = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const isActive = ref === branch.project_ref
|
||||
const daysFromNow = dayjs().diff(dayjs(branch.created_at), 'day')
|
||||
const formattedTimeFromNow = dayjs(branch.created_at).fromNow()
|
||||
const formattedCreatedAt = dayjs(branch.created_at).format('DD MMM YYYY, HH:mm:ss (ZZ)')
|
||||
|
||||
return (
|
||||
<BranchContainer className="bg-surface-100">
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconGitBranch className="text-brand-900" size={16} strokeWidth={2} />
|
||||
<p>{branch.name}</p>
|
||||
{isActive && <Badge color="green">Selected</Badge>}
|
||||
<p className="text-scale-1000">
|
||||
{daysFromNow > 1 ? `Created on ${formattedCreatedAt}` : `Created ${formattedTimeFromNow}`}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<DropdownMenu_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
||||
<DropdownMenuTrigger_Shadcn_>
|
||||
<Button asChild type="text" className="px-1" icon={<IconMoreVertical size={14} />}>
|
||||
<span></span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger_Shadcn_>
|
||||
<DropdownMenuContent_Shadcn_ side="bottom" align="end">
|
||||
<DropdownMenuItem_Shadcn_ className="flex gap-2" onSelect={() => onSelectUpdate()}>
|
||||
<IconEdit size={14} />
|
||||
Edit branch
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
<DropdownMenuItem_Shadcn_ className="flex gap-2" onSelect={() => onSelectDelete()}>
|
||||
<IconTrash size={14} />
|
||||
Delete branch
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</DropdownMenuContent_Shadcn_>
|
||||
</DropdownMenu_Shadcn_>
|
||||
</div>
|
||||
</BranchContainer>
|
||||
)
|
||||
}
|
||||
|
||||
interface BranchHeader extends React.HTMLAttributes<HTMLDivElement> {
|
||||
name?: string
|
||||
markdown?: string
|
||||
}
|
||||
|
||||
const BranchHeader = forwardRef<HTMLDivElement, BranchHeader>(
|
||||
({ className, name, markdown = '', ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-l border-scale-600 dark:border-scale-400 ml-6 pl-8 pt-6 pb-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Markdown content={markdown} className="" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BranchHeader.displayName = 'BranchHeader'
|
||||
export { BranchHeader, BranchContainer, BranchPanel, MainBranchPanel }
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useRef } from 'react'
|
||||
import { Button, Form, Input, Listbox, SidePanel } from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
|
||||
import { useSelectedProject, useStore } from 'hooks'
|
||||
|
||||
interface CreateBranchSidePanelProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const MOCK_BRANCHES = [
|
||||
{ id: 1, name: 'feat/branch-01' },
|
||||
{ id: 2, name: 'feat/branch-02' },
|
||||
]
|
||||
|
||||
const CreateBranchSidePanel = ({ visible, onClose }: CreateBranchSidePanelProps) => {
|
||||
const { ui } = useStore()
|
||||
const submitRef: any = useRef()
|
||||
const { ref } = useParams()
|
||||
const projectDetails = useSelectedProject()
|
||||
|
||||
const isBranch = projectDetails?.parent_project_ref !== undefined
|
||||
const projectRef =
|
||||
projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
|
||||
|
||||
const formId = 'create-branch-form'
|
||||
const initialValues = { branchName: '', gitBranch: 'no-selection' }
|
||||
const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({
|
||||
onSuccess: () => {
|
||||
ui.setNotification({ category: 'success', message: `Successfully created new branch` })
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
if (values.branchName.length === 0)
|
||||
errors.branchName = 'Please provide a name for your preview branch'
|
||||
if (values.gitBranch.length === 0 || values.gitBranch === 'no-selection')
|
||||
errors.gitBranch = 'Please select a Git branch to link to this database preview branch'
|
||||
return errors
|
||||
}
|
||||
|
||||
const onConfirmCreate = (values: any) => {
|
||||
if (!projectRef) return console.error('Project ref is required')
|
||||
createBranch({ projectRef, ...values })
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
visible={visible}
|
||||
loading={isCreating}
|
||||
onCancel={onClose}
|
||||
header="Create a new database preview branch"
|
||||
customFooter={
|
||||
<div className="flex w-full justify-end space-x-3 border-t border-scale-500 px-3 py-4">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClose}
|
||||
disabled={isCreating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
htmlType="button"
|
||||
disabled={isCreating}
|
||||
loading={isCreating}
|
||||
onClick={() => submitRef?.current?.click()}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="py-6">
|
||||
<SidePanel.Content>
|
||||
<Form
|
||||
validateOnBlur
|
||||
id={formId}
|
||||
initialValues={initialValues}
|
||||
validate={validate}
|
||||
onSubmit={onConfirmCreate}
|
||||
>
|
||||
{() => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Input id="branchName" label="Branch name" />
|
||||
<Listbox size="medium" id="gitBranch" name="gitBranch" label="Git branch">
|
||||
<Listbox.Option
|
||||
label="---"
|
||||
key="no-selection"
|
||||
id="no-selection"
|
||||
value="no-selection"
|
||||
>
|
||||
---
|
||||
</Listbox.Option>
|
||||
{MOCK_BRANCHES.map((branch) => (
|
||||
<Listbox.Option
|
||||
key={branch.id}
|
||||
id={branch.id.toString()}
|
||||
value={branch.name}
|
||||
label={branch.name}
|
||||
>
|
||||
<p className="text-scale-1200">{branch.name}</p>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
|
||||
<button ref={submitRef} type="submit" className="hidden" />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateBranchSidePanel
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
import { BranchContainer, BranchHeader, BranchPanel } from './BranchPanels'
|
||||
import { Button, IconExternalLink, IconGitBranch } from 'ui'
|
||||
|
||||
interface PreviewBranchesProps {
|
||||
previewBranches: Branch[]
|
||||
onSelectUpdateBranch: (branch: Branch) => void
|
||||
onSelectDeleteBranch: (branch: Branch) => void
|
||||
}
|
||||
|
||||
const PreviewBranches = ({
|
||||
previewBranches,
|
||||
onSelectUpdateBranch,
|
||||
onSelectDeleteBranch,
|
||||
}: PreviewBranchesProps) => {
|
||||
return (
|
||||
<>
|
||||
<BranchHeader markdown={previewBranches.length > 0 ? `#### Preview branches` : undefined} />
|
||||
{previewBranches.length === 0 ? (
|
||||
<BranchContainer>
|
||||
<div className="flex items-center flex-col justify-center w-full py-8">
|
||||
<p>No database preview branches</p>
|
||||
<p className="text-scale-1000">Database preview branches will be shown here</p>
|
||||
<div className="w-[500px] border rounded-md mt-4">
|
||||
<div className="px-5 py-3 bg-surface-100 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconGitBranch strokeWidth={2} className="text-scale-1100" />
|
||||
<div>
|
||||
<p>Create a preview branch</p>
|
||||
<p className="text-scale-1000">Start developing in preview</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="default">Create preview branch</Button>
|
||||
</div>
|
||||
<div className="px-5 py-3 border-t flex items-center justify-between">
|
||||
<div>
|
||||
<p>Not sure what to do?</p>
|
||||
<p className="text-scale-1000">Browse our documentation</p>
|
||||
</div>
|
||||
<Button type="default" iconRight={<IconExternalLink />}>
|
||||
Docs
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BranchContainer>
|
||||
) : (
|
||||
previewBranches.map((branch) => (
|
||||
<BranchPanel
|
||||
key={branch.id}
|
||||
branch={branch}
|
||||
onSelectUpdate={() => onSelectUpdateBranch(branch)}
|
||||
onSelectDelete={() => onSelectDeleteBranch(branch)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreviewBranches
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
import { BranchContainer, BranchHeader } from './BranchPanels'
|
||||
import { Button, IconExternalLink, IconGitBranch } from 'ui'
|
||||
|
||||
interface PullRequestsProps {
|
||||
previewBranches: Branch[]
|
||||
}
|
||||
|
||||
const PullRequests = ({ previewBranches }: PullRequestsProps) => {
|
||||
const pullRequests = []
|
||||
|
||||
return (
|
||||
<>
|
||||
<BranchHeader
|
||||
markdown={
|
||||
pullRequests.length > 0 && previewBranches.length > 0
|
||||
? `#### Open pull requests`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{previewBranches.length === 0 || pullRequests.length === 0 ? (
|
||||
<BranchContainer>
|
||||
<div className="flex items-center flex-col justify-center w-full py-8">
|
||||
<p>No pull requests made yet for this repository</p>
|
||||
<p className="text-scale-1000">
|
||||
Only pull requests with the ./migration directory changes will show here.
|
||||
</p>
|
||||
{previewBranches.length > 0 && (
|
||||
<div className="w-96 border rounded-md mt-4">
|
||||
<div className="px-5 py-3 bg-surface-100 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconGitBranch strokeWidth={2} className="text-scale-1100" />
|
||||
<p>Create a pull request</p>
|
||||
</div>
|
||||
<Button type="default" iconRight={<IconExternalLink />}>
|
||||
Github
|
||||
</Button>
|
||||
</div>
|
||||
<div className="px-5 py-3 border-t flex items-center justify-between">
|
||||
<div>
|
||||
<p>Not sure what to do?</p>
|
||||
<p className="text-scale-1000">Browse our documentation</p>
|
||||
</div>
|
||||
<Button type="default" iconRight={<IconExternalLink />}>
|
||||
Docs
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BranchContainer>
|
||||
) : (
|
||||
<BranchContainer>State 3</BranchContainer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PullRequests
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useParams } from 'common'
|
||||
import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { useRef } from 'react'
|
||||
import { Button, Form, Input, SidePanel } from 'ui'
|
||||
|
||||
interface UpdateBranchSidePanelProps {
|
||||
selectedBranch?: Branch
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const UpdateBranchSidePanel = ({ selectedBranch, onClose }: UpdateBranchSidePanelProps) => {
|
||||
const { ui } = useStore()
|
||||
const submitRef: any = useRef()
|
||||
const { ref: projectRef } = useParams()
|
||||
const { mutate: updateBranch, isLoading: isUpdating } = useBranchUpdateMutation({
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
ui.setNotification({ category: 'success', message: 'Successfully updated branch' })
|
||||
},
|
||||
})
|
||||
|
||||
const formId = 'update-branch-form'
|
||||
const initialValues = {
|
||||
branchName: selectedBranch?.name,
|
||||
gitBranch: selectedBranch?.git_branch,
|
||||
}
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
if (values.branchName.length === 0) errors.branchName = 'Please provide a branch name'
|
||||
if (values.gitBranch.length === 0) errors.gitBranch = 'Please provide a Git branch'
|
||||
return errors
|
||||
}
|
||||
|
||||
const onConfirmUpdate = (values: any) => {
|
||||
if (selectedBranch === undefined) return console.error('No branch selected')
|
||||
if (projectRef === undefined) return console.error('Project ref is required')
|
||||
updateBranch({ ...values, id: selectedBranch.id, projectRef })
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
visible={selectedBranch !== undefined}
|
||||
header={`Update branch: ${selectedBranch?.name}`}
|
||||
onCancel={onClose}
|
||||
customFooter={
|
||||
<div className="flex w-full justify-end space-x-3 border-t border-scale-500 px-3 py-4">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClose}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
htmlType="button"
|
||||
disabled={isUpdating}
|
||||
loading={isUpdating}
|
||||
onClick={() => submitRef?.current?.click()}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="py-6">
|
||||
<SidePanel.Content>
|
||||
<Form
|
||||
validateOnBlur
|
||||
id={formId}
|
||||
initialValues={initialValues}
|
||||
validate={validate}
|
||||
onSubmit={onConfirmUpdate}
|
||||
>
|
||||
{() => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Input id="branchName" label="Branch name" />
|
||||
<Input disabled id="gitBranch" label="Git branch" />
|
||||
<button ref={submitRef} type="submit" className="hidden" />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateBranchSidePanel
|
||||
@@ -36,6 +36,7 @@ const BackupsList = () => {
|
||||
primaryText="Free Plan does not include project backups."
|
||||
projectRef={projectRef}
|
||||
secondaryText="Upgrade to the Pro plan for up to 7 days of scheduled backups."
|
||||
addon="pitr"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { Integration, IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { getVercelConfigurationUrl } from 'lib/integration-utils'
|
||||
import { Badge, Button, IconArrowRight, IconExternalLink, IconGitHub, IconSquare, cn } from 'ui'
|
||||
import { getIntegrationConfigurationUrl } from 'lib/integration-utils'
|
||||
|
||||
const ICON_STROKE_WIDTH = 2
|
||||
const ICON_SIZE = 14
|
||||
@@ -24,9 +24,9 @@ const HandleIcon = ({ type, className }: { type: HandleIconType; className?: str
|
||||
case 'GitHub':
|
||||
return <IconGitHub strokeWidth={ICON_STROKE_WIDTH} size={ICON_SIZE} />
|
||||
break
|
||||
case 'Netlify':
|
||||
return <IconSquare strokeWidth={ICON_STROKE_WIDTH} size={ICON_SIZE} />
|
||||
break
|
||||
// case 'Netlify':
|
||||
// return <IconSquare strokeWidth={ICON_STROKE_WIDTH} size={ICON_SIZE} />
|
||||
// break
|
||||
case 'Vercel':
|
||||
return (
|
||||
<svg
|
||||
@@ -92,8 +92,10 @@ const IntegrationInstallation = React.forwardRef<HTMLLIElement, IntegrationInsta
|
||||
<div className="flex flex-col gap-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-scale-1200 font-medium">
|
||||
{/* {title} integration connection •{' '} */}
|
||||
{integration.metadata?.account.name || integration.metadata?.gitHubConnectionOwner}
|
||||
{integration.metadata?.account.name ||
|
||||
(integration.metadata !== undefined &&
|
||||
'gitHubConnectionOwner' in integration.metadata &&
|
||||
integration.metadata?.gitHubConnectionOwner)}
|
||||
</span>
|
||||
|
||||
<Badge color="scale" className="capitalize">
|
||||
@@ -112,9 +114,8 @@ const IntegrationInstallation = React.forwardRef<HTMLLIElement, IntegrationInsta
|
||||
</div>
|
||||
|
||||
<Button type="default" asChild iconRight={<IconExternalLink />}>
|
||||
{/* hard coded to vercel for now, TODO: move to a prop */}
|
||||
<a
|
||||
href={getVercelConfigurationUrl(integration)}
|
||||
href={getIntegrationConfigurationUrl(integration)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -153,7 +154,7 @@ const IntegrationConnection = React.forwardRef<HTMLLIElement, IntegrationConnect
|
||||
<IconArrowRight size={14} className="text-scale-900" strokeWidth={1.5} />
|
||||
{!connection?.metadata?.framework ? (
|
||||
<div className="bg-black text-white w-4 h-4 rounded flex items-center justify-center">
|
||||
<HandleIcon type={'Vercel'} className={'!w-2.5'} />
|
||||
<HandleIcon type={type} className={'!w-2.5'} />
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { getHasInstalledObject } from 'components/layouts/IntegrationsLayout/Integrations.utils'
|
||||
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
|
||||
import { IntegrationName } from 'data/integrations/integrations.types'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { Organization } from 'types'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
IconChevronDown,
|
||||
IconHexagon,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
export interface OrganizationPickerProps {
|
||||
integrationName: IntegrationName
|
||||
configurationId?: string
|
||||
selectedOrg: Organization | null
|
||||
onSelectedOrgChange: (organization: Organization) => void
|
||||
}
|
||||
|
||||
const OrganizationPicker = ({
|
||||
integrationName,
|
||||
configurationId,
|
||||
selectedOrg,
|
||||
onSelectedOrgChange,
|
||||
}: OrganizationPickerProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const { data: integrationData } = useIntegrationsQuery()
|
||||
const { data: organizationsData, isLoading: isLoadingOrganization } = useOrganizationsQuery()
|
||||
|
||||
const installed = useMemo(
|
||||
() =>
|
||||
integrationData && organizationsData
|
||||
? getHasInstalledObject({
|
||||
integrationName,
|
||||
integrationData,
|
||||
organizationsData,
|
||||
installationId: configurationId,
|
||||
})
|
||||
: {},
|
||||
[configurationId, integrationData, integrationName, organizationsData]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
type="default"
|
||||
size="medium"
|
||||
block
|
||||
className="justify-start"
|
||||
icon={<IconHexagon />}
|
||||
loading={isLoadingOrganization}
|
||||
disabled={isLoadingOrganization}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
<IconChevronDown className={''} />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span className="flex gap-2">
|
||||
<span className="truncate">{selectedOrg?.name}</span>
|
||||
{selectedOrg && configurationId && installed[selectedOrg.slug] && (
|
||||
<Badge color="scale">Integration Installed</Badge>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0 w-full"
|
||||
side="bottom"
|
||||
align="center"
|
||||
style={{ width: ref.current?.offsetWidth }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search organizations..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{organizationsData?.map((org) => {
|
||||
return (
|
||||
<CommandItem_Shadcn_
|
||||
value={org.slug}
|
||||
key={org.slug}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={(slug) => {
|
||||
const org = organizationsData?.find(
|
||||
(org) => org.slug.toLowerCase() === slug.toLowerCase()
|
||||
)
|
||||
if (org) {
|
||||
onSelectedOrgChange(org)
|
||||
}
|
||||
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<IconHexagon />
|
||||
<span className="truncate">{org.name}</span>{' '}
|
||||
{configurationId && installed[org.slug] && (
|
||||
<Badge color="scale" className="!flex-none">
|
||||
Integration Installed
|
||||
</Badge>
|
||||
)}
|
||||
</CommandItem_Shadcn_>
|
||||
)
|
||||
})}
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrganizationPicker
|
||||
@@ -1,14 +1,9 @@
|
||||
import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { vercelIcon } from 'components/to-be-cleaned/ListIcons'
|
||||
import { useIntegrationConnectionsCreateMutation } from 'data/integrations/integration-connections-create-mutation'
|
||||
import { useIntegrationsVercelConnectionSyncEnvsMutation } from 'data/integrations/integrations-vercel-connection-sync-envs-mutation'
|
||||
import { VercelProjectsResponse } from 'data/integrations/integrations-vercel-projects-query'
|
||||
import { ReactNode, useRef, useState } from 'react'
|
||||
|
||||
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { IntegrationConnectionsCreateVariables } from 'data/integrations/types'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
@@ -24,20 +19,28 @@ import {
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
interface Project {
|
||||
export interface Project {
|
||||
id: string
|
||||
name: string
|
||||
ref: string
|
||||
}
|
||||
|
||||
export interface ForeignProject {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ProjectLinkerProps {
|
||||
organizationIntegrationId: string | undefined
|
||||
foreignProjects: VercelProjectsResponse[]
|
||||
foreignProjects: ForeignProject[]
|
||||
supabaseProjects: Project[]
|
||||
onCreateConnections?: () => void
|
||||
onCreateConnections: (variables: IntegrationConnectionsCreateVariables) => void
|
||||
installedConnections: IntegrationProjectConnection[] | undefined
|
||||
setLoading?: (x: boolean) => void
|
||||
showSkip?: boolean
|
||||
isLoading?: boolean
|
||||
integrationIcon: ReactNode
|
||||
getForeignProjectIcon?: (project: ForeignProject) => ReactNode
|
||||
choosePrompt?: string
|
||||
onSkip?: () => void
|
||||
loadingForeignProjects?: boolean
|
||||
loadingSupabaseProjects?: boolean
|
||||
}
|
||||
@@ -48,77 +51,54 @@ const ProjectLinker = ({
|
||||
supabaseProjects,
|
||||
onCreateConnections: _onCreateConnections,
|
||||
installedConnections = [],
|
||||
setLoading,
|
||||
showSkip = false,
|
||||
isLoading,
|
||||
integrationIcon,
|
||||
getForeignProjectIcon,
|
||||
choosePrompt = 'Choose a project',
|
||||
onSkip,
|
||||
loadingForeignProjects,
|
||||
loadingSupabaseProjects,
|
||||
}: ProjectLinkerProps) => {
|
||||
const [supabaseProjectsComboBoxOpen, setSupabaseProjectsComboboxOpen] = useState(false)
|
||||
const [vercelProjectsComboBoxOpen, setVercelProjectsComboboxOpen] = useState(false)
|
||||
const [foreignProjectsComboBoxOpen, setForeignProjectsComboboxOpen] = useState(false)
|
||||
const supabaseProjectsComboBoxRef = useRef<HTMLButtonElement>(null)
|
||||
const vercelProjectsComboBoxRef = useRef<HTMLButtonElement>(null)
|
||||
const foreignProjectsComboBoxRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const [supabaseProjectRef, setSupabaseProjectRef] = useState<string | undefined>(undefined)
|
||||
const [vercelProjectId, setVercelProjectId] = useState<string | undefined>(undefined)
|
||||
|
||||
const { mutateAsync: syncEnvs } = useIntegrationsVercelConnectionSyncEnvsMutation()
|
||||
const { mutate: createConnections, isLoading } = useIntegrationConnectionsCreateMutation({
|
||||
async onSuccess({ id }) {
|
||||
try {
|
||||
await syncEnvs({ connectionId: id })
|
||||
} catch (error: any) {
|
||||
toast.error('Failed to sync environment variables: ', error.message)
|
||||
}
|
||||
|
||||
if (setLoading) setLoading(false)
|
||||
_onCreateConnections?.()
|
||||
},
|
||||
onError() {
|
||||
if (setLoading) setLoading(false)
|
||||
},
|
||||
})
|
||||
const [foreignProjectId, setForeignProjectId] = useState<string | undefined>(undefined)
|
||||
|
||||
// create a flat array of foreign project ids. ie, ["prj_MlkO6AiLG5ofS9ojKrkS3PhhlY3f", ..]
|
||||
const flatInstalledConnectionsIds = new Set(installedConnections.map((x) => x.foreign_project_id))
|
||||
|
||||
// check that vercel project is not already installed
|
||||
const filteredForeignProjects: VercelProjectsResponse[] = foreignProjects.filter(
|
||||
(foreignProject) => {
|
||||
return !flatInstalledConnectionsIds.has(foreignProject.id)
|
||||
}
|
||||
)
|
||||
// check that foreign project is not already installed
|
||||
const filteredForeignProjects = foreignProjects.filter((foreignProject) => {
|
||||
return !flatInstalledConnectionsIds.has(foreignProject.id)
|
||||
})
|
||||
|
||||
const selectedSupabaseProject = supabaseProjectRef
|
||||
? supabaseProjects.find((x) => x.ref?.toLowerCase() === supabaseProjectRef?.toLowerCase())
|
||||
: undefined
|
||||
|
||||
const selectedVercelProject = vercelProjectId
|
||||
? filteredForeignProjects.find((x) => x.id?.toLowerCase() === vercelProjectId?.toLowerCase())
|
||||
const selectedForeignProject = foreignProjectId
|
||||
? filteredForeignProjects.find((x) => x.id?.toLowerCase() === foreignProjectId?.toLowerCase())
|
||||
: undefined
|
||||
|
||||
function onCreateConnections() {
|
||||
const projectDetails = selectedVercelProject
|
||||
const projectDetails = selectedForeignProject
|
||||
|
||||
if (!organizationIntegrationId) return console.error('No integration ID set')
|
||||
if (!selectedVercelProject?.id) return console.error('No Vercel project ID set')
|
||||
if (!selectedForeignProject?.id) return console.error('No Foreign project ID set')
|
||||
if (!selectedSupabaseProject?.ref) return console.error('No Supabase project ref set')
|
||||
|
||||
if (setLoading) setLoading(true)
|
||||
|
||||
createConnections({
|
||||
_onCreateConnections({
|
||||
organizationIntegrationId,
|
||||
connection: {
|
||||
foreign_project_id: selectedVercelProject?.id,
|
||||
foreign_project_id: selectedForeignProject?.id,
|
||||
supabase_project_ref: selectedSupabaseProject?.ref,
|
||||
metadata: {
|
||||
...projectDetails,
|
||||
supabaseConfig: {
|
||||
projectEnvVars: {
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orgSlug: selectedOrganization?.slug,
|
||||
@@ -225,23 +205,16 @@ const ProjectLinker = ({
|
||||
<div className="border border-scale-1000 h-px w-16 border-dashed self-end mb-5"></div>
|
||||
<Panel>
|
||||
<div className="bg-black shadow rounded p-1 w-12 h-12 flex justify-center items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="white"
|
||||
viewBox="0 0 512 512"
|
||||
className="w-6"
|
||||
>
|
||||
<path fillRule="evenodd" d="M256,48,496,464H16Z" />
|
||||
</svg>
|
||||
{integrationIcon}
|
||||
</div>
|
||||
|
||||
<Popover_Shadcn_
|
||||
open={vercelProjectsComboBoxOpen}
|
||||
onOpenChange={setVercelProjectsComboboxOpen}
|
||||
open={foreignProjectsComboBoxOpen}
|
||||
onOpenChange={setForeignProjectsComboboxOpen}
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
ref={vercelProjectsComboBoxRef}
|
||||
ref={foreignProjectsComboBoxRef}
|
||||
type="default"
|
||||
size="medium"
|
||||
block
|
||||
@@ -249,20 +222,9 @@ const ProjectLinker = ({
|
||||
loading={loadingForeignProjects}
|
||||
className="justify-start"
|
||||
icon={
|
||||
selectedVercelProject ? (
|
||||
selectedVercelProject?.framework ? (
|
||||
vercelIcon
|
||||
) : (
|
||||
<img
|
||||
src={`${BASE_PATH}/img/icons/frameworks/${selectedVercelProject.framework}.svg`}
|
||||
width={21}
|
||||
height={21}
|
||||
alt={`icon`}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
selectedForeignProject
|
||||
? getForeignProjectIcon?.(selectedForeignProject)
|
||||
: integrationIcon
|
||||
}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
@@ -270,15 +232,14 @@ const ProjectLinker = ({
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(selectedVercelProject && selectedVercelProject.name) ??
|
||||
'Choose a Vercel Project'}
|
||||
{(selectedForeignProject && selectedForeignProject.name) ?? choosePrompt}
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0 w-full"
|
||||
side="bottom"
|
||||
align="center"
|
||||
style={{ width: vercelProjectsComboBoxRef.current?.offsetWidth }}
|
||||
style={{ width: foreignProjectsComboBoxRef.current?.offsetWidth }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search organization..." />
|
||||
@@ -292,20 +253,11 @@ const ProjectLinker = ({
|
||||
key={project.id}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={(id) => {
|
||||
if (id) setVercelProjectId(id)
|
||||
setVercelProjectsComboboxOpen(false)
|
||||
if (id) setForeignProjectId(id)
|
||||
setForeignProjectsComboboxOpen(false)
|
||||
}}
|
||||
>
|
||||
{!project?.framework ? (
|
||||
vercelIcon
|
||||
) : (
|
||||
<img
|
||||
src={`${BASE_PATH}/img/icons/frameworks/${project.framework}.svg`}
|
||||
width={21}
|
||||
height={21}
|
||||
alt={`icon`}
|
||||
/>
|
||||
)}
|
||||
{getForeignProjectIcon?.(project) ?? integrationIcon}
|
||||
<span>{project.name}</span>
|
||||
</CommandItem_Shadcn_>
|
||||
)
|
||||
@@ -319,12 +271,12 @@ const ProjectLinker = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end gap-2">
|
||||
{showSkip && (
|
||||
{onSkip !== undefined && (
|
||||
<Button
|
||||
size="medium"
|
||||
type="default"
|
||||
onClick={() => {
|
||||
_onCreateConnections?.()
|
||||
onSkip()
|
||||
}}
|
||||
>
|
||||
Skip
|
||||
@@ -340,26 +292,14 @@ const ProjectLinker = ({
|
||||
loadingForeignProjects ||
|
||||
loadingSupabaseProjects ||
|
||||
isLoading ||
|
||||
// check wether both project types are not undefined
|
||||
// check whether both project types are not undefined
|
||||
!selectedSupabaseProject ||
|
||||
!selectedVercelProject
|
||||
!selectedForeignProject
|
||||
}
|
||||
>
|
||||
Connect project
|
||||
</Button>
|
||||
</div>
|
||||
<Markdown
|
||||
content={`
|
||||
The following environment variables will be added:
|
||||
|
||||
${ENV_VAR_RAW_KEYS.map((x, idx) => {
|
||||
return `
|
||||
\n
|
||||
- \`${x}\`
|
||||
`
|
||||
})}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ interface Props extends Omit<ReactMarkdownProps, 'children' | 'node'> {
|
||||
|
||||
const Markdown = ({ className, content, ...props }: Props) => {
|
||||
return (
|
||||
<ReactMarkdown {...props} className={cn('prose text-sm', className)}>
|
||||
<ReactMarkdown
|
||||
components={{ h3: ({ children }) => <h3 className="mb-1">{children}</h3> }}
|
||||
{...props}
|
||||
className={cn('prose text-sm', className)}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
)
|
||||
|
||||
@@ -11,42 +11,37 @@ import {
|
||||
ScaffoldSectionContent,
|
||||
ScaffoldSectionDetail,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import { useIntegrationsVercelInstalledConnectionDeleteMutation } from 'data/integrations/integrations-vercel-installed-connection-delete-mutation'
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
import {
|
||||
IntegrationName,
|
||||
IntegrationProjectConnection,
|
||||
Integration as TIntegration,
|
||||
} from 'data/integrations/integrations.types'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { pluralize } from 'lib/helpers'
|
||||
import { EMPTY_ARR } from 'lib/void'
|
||||
import { useGithubConnectionConfigPanelSnapshot } from 'state/github-connection-config-panel'
|
||||
import { Button, Dropdown, IconChevronDown, IconTrash } from 'ui'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Button, Dropdown, IconChevronDown, IconTrash, Modal } from 'ui'
|
||||
|
||||
export interface IntegrationProps {
|
||||
title: string
|
||||
orgName?: string
|
||||
description?: string
|
||||
note?: string
|
||||
detail?: string
|
||||
integrations?: TIntegration[]
|
||||
onAddConnection: (integrationId: string) => void
|
||||
onDeleteConnection: (connection: IntegrationProjectConnection) => void | Promise<void>
|
||||
}
|
||||
|
||||
const Integration = ({
|
||||
title,
|
||||
orgName,
|
||||
description,
|
||||
note,
|
||||
detail,
|
||||
integrations = EMPTY_ARR,
|
||||
onAddConnection,
|
||||
onDeleteConnection,
|
||||
}: IntegrationProps) => {
|
||||
const snapshot = useGithubConnectionConfigPanelSnapshot()
|
||||
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const { mutate: deleteMutate, isLoading: isDeleteLoading } =
|
||||
useIntegrationsVercelInstalledConnectionDeleteMutation()
|
||||
|
||||
const ConnectionHeading = ({ integration }: { integration: TIntegration }) => {
|
||||
return (
|
||||
<IntegrationConnectionHeader
|
||||
@@ -60,17 +55,6 @@ Repository connections for ${title?.toLowerCase()}
|
||||
)
|
||||
}
|
||||
|
||||
// Call the mutation when needed
|
||||
const handleDelete = (organizationIntegrationId: string, projectIntegrationId: string) => {
|
||||
const variables = {
|
||||
organization_integration_id: organizationIntegrationId,
|
||||
id: projectIntegrationId,
|
||||
orgSlug: selectedOrganization?.slug,
|
||||
}
|
||||
|
||||
deleteMutate(variables)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
@@ -89,60 +73,28 @@ Repository connections for ${title?.toLowerCase()}
|
||||
{integrations.length > 0 &&
|
||||
integrations.map((integration, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<IntegrationInstallation title={title} key={i} integration={integration} />
|
||||
<div key={integration.id}>
|
||||
<IntegrationInstallation title={title} integration={integration} />
|
||||
|
||||
{integration.connections.length > 0 ? (
|
||||
<>
|
||||
<ConnectionHeading integration={integration} />
|
||||
|
||||
<ul className="flex flex-col">
|
||||
{integration.connections.map(
|
||||
(connection: IntegrationProjectConnection, i) => (
|
||||
<IntegrationConnection
|
||||
key={i}
|
||||
connection={connection}
|
||||
type={integration.integration.name}
|
||||
actions={
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
size="large"
|
||||
overlay={
|
||||
<>
|
||||
<Dropdown.Item
|
||||
icon={<IconTrash size={14} />}
|
||||
onSelect={() =>
|
||||
handleDelete(integration.id, connection.id)
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
iconRight={<IconChevronDown />}
|
||||
type="default"
|
||||
>
|
||||
<span>Manage</span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{integration.connections.map((connection) => (
|
||||
<IntegrationConnectionItem
|
||||
key={connection.id}
|
||||
connection={connection}
|
||||
type={title as IntegrationName}
|
||||
onDeleteConnection={onDeleteConnection}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<ConnectionHeading integration={integration} />
|
||||
)}
|
||||
<EmptyIntegrationConnection
|
||||
onClick={() => {
|
||||
snapshot.setOrganizationIntegrationId(integration.id)
|
||||
snapshot.setOpen(true)
|
||||
}}
|
||||
>
|
||||
<EmptyIntegrationConnection onClick={() => onAddConnection(integration.id)}>
|
||||
Add new project connection
|
||||
</EmptyIntegrationConnection>
|
||||
</div>
|
||||
@@ -158,3 +110,71 @@ Repository connections for ${title?.toLowerCase()}
|
||||
}
|
||||
|
||||
export default Integration
|
||||
|
||||
type IntegrationConnectionItemProps = {
|
||||
connection: IntegrationProjectConnection
|
||||
type: IntegrationName
|
||||
onDeleteConnection: (connection: IntegrationProjectConnection) => void | Promise<void>
|
||||
}
|
||||
|
||||
const IntegrationConnectionItem = ({
|
||||
connection,
|
||||
type,
|
||||
onDeleteConnection,
|
||||
}: IntegrationConnectionItemProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const onConfirm = useCallback(async () => {
|
||||
try {
|
||||
await onDeleteConnection(connection)
|
||||
} finally {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [connection, onDeleteConnection])
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegrationConnection
|
||||
connection={connection}
|
||||
type={type}
|
||||
actions={
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="end"
|
||||
size="large"
|
||||
overlay={
|
||||
<>
|
||||
<Dropdown.Item icon={<IconTrash size={14} />} onSelect={() => setIsOpen(true)}>
|
||||
Delete
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button asChild iconRight={<IconChevronDown />} type="default">
|
||||
<span>Manage</span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isOpen}
|
||||
danger={true}
|
||||
header="Confirm to delete"
|
||||
buttonLabel="Delete"
|
||||
onSelectCancel={onCancel}
|
||||
onSelectConfirm={onConfirm}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-light">
|
||||
{`This action cannot be undone. Are you sure you want to delete this connection?`}
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,25 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { ScaffoldDivider } from 'components/layouts/Scaffold'
|
||||
import { useIntegrationsGitHubInstalledConnectionDeleteMutation } from 'data/integrations/integrations-github-connection-delete-mutation'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useIntegrationsVercelInstalledConnectionDeleteMutation } from 'data/integrations/integrations-vercel-installed-connection-delete-mutation'
|
||||
import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { getVercelConfigurationUrl } from 'lib/integration-utils'
|
||||
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { getIntegrationConfigurationUrl } from 'lib/integration-utils'
|
||||
import Integration from './Integration'
|
||||
import SidePanelGitHubRepoLinker from './SidePanelGitHubRepoLinker'
|
||||
import SidePanelVercelProjectLinker from './SidePanelVercelProjectLinker'
|
||||
|
||||
const IntegrationSettings = () => {
|
||||
const org = useSelectedOrganization()
|
||||
const { data } = useOrgIntegrationsQuery({ orgSlug: org?.slug })
|
||||
const { slug } = useParams()
|
||||
const { data } = useOrgIntegrationsQuery({ orgSlug: slug })
|
||||
|
||||
const vercelIntegrations = data
|
||||
?.filter((integration) => integration.integration.name === 'Vercel')
|
||||
.map((integration) => {
|
||||
if (integration.metadata) {
|
||||
if (integration.metadata && integration.integration.name === 'Vercel') {
|
||||
const avatarSrc =
|
||||
!integration.metadata.account.avatar && integration.metadata.account.type === 'Team'
|
||||
? `https://vercel.com/api/www/avatar?teamId=${integration.metadata.account.team_id}&s=48`
|
||||
@@ -39,11 +45,56 @@ const IntegrationSettings = () => {
|
||||
)
|
||||
const vercelProjectCount = vercelProjectsData?.length ?? 0
|
||||
|
||||
const [addPanelOpen, setAddPanelOpen] = useState<undefined | 'VERCEL' | 'GITHUB'>(undefined)
|
||||
const [addPanelIntegrationId, setAddPanelIntegrationId] = useState<string | undefined>(undefined)
|
||||
|
||||
const onAddVercelConnection = useCallback((integrationId: string) => {
|
||||
setAddPanelIntegrationId(integrationId)
|
||||
setAddPanelOpen('VERCEL')
|
||||
}, [])
|
||||
|
||||
const onAddGitHubConnection = useCallback((integrationId: string) => {
|
||||
setAddPanelIntegrationId(integrationId)
|
||||
setAddPanelOpen('GITHUB')
|
||||
}, [])
|
||||
|
||||
const onCloseAddPanel = useCallback(() => {
|
||||
setAddPanelOpen(undefined)
|
||||
setAddPanelIntegrationId(undefined)
|
||||
}, [])
|
||||
|
||||
const { mutateAsync: deleteVercelConnection } =
|
||||
useIntegrationsVercelInstalledConnectionDeleteMutation()
|
||||
|
||||
const onDeleteVercelConnection = useCallback(
|
||||
async (connection: IntegrationProjectConnection) => {
|
||||
await deleteVercelConnection({
|
||||
id: connection.id,
|
||||
organization_integration_id: connection.organization_integration_id,
|
||||
orgSlug: slug,
|
||||
})
|
||||
},
|
||||
[deleteVercelConnection, slug]
|
||||
)
|
||||
|
||||
const { mutateAsync: deleteGitHubConnection } =
|
||||
useIntegrationsGitHubInstalledConnectionDeleteMutation()
|
||||
|
||||
const onDeleteGitHubConnection = useCallback(
|
||||
async (connection: IntegrationProjectConnection) => {
|
||||
await deleteGitHubConnection({
|
||||
connectionId: connection.id,
|
||||
integrationId: connection.organization_integration_id,
|
||||
orgSlug: slug,
|
||||
})
|
||||
},
|
||||
[deleteGitHubConnection, slug]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Integration
|
||||
title="Vercel"
|
||||
orgName={org?.name}
|
||||
detail={`
|
||||
## Vercel Integration
|
||||
|
||||
@@ -59,26 +110,25 @@ You can also link multiple Vercel Projects to the same Supabase project.
|
||||
vercelProjectCount > 0 && vercelIntegration !== undefined
|
||||
? `
|
||||
Your Vercel connection has access to ${vercelProjectCount} Vercel Projects.
|
||||
You can change the scope of the access for Supabase by configuring [here](${getVercelConfigurationUrl(
|
||||
You can change the scope of the access for Supabase by configuring [here](${getIntegrationConfigurationUrl(
|
||||
vercelIntegration
|
||||
)}).
|
||||
`
|
||||
: undefined
|
||||
}
|
||||
integrations={vercelIntegrations}
|
||||
onAddConnection={onAddVercelConnection}
|
||||
onDeleteConnection={onDeleteVercelConnection}
|
||||
/>
|
||||
<ScaffoldDivider />
|
||||
<Integration
|
||||
title="GitHub"
|
||||
orgName={org?.name}
|
||||
detail={`
|
||||
## GitHub Connections
|
||||
|
||||
Connect any of your GitHub repositories to a project.
|
||||
`}
|
||||
description={`
|
||||
GitHub connections are coming soon.
|
||||
|
||||
### How will GitHub connections work?
|
||||
|
||||
You will be able to connect a GitHub repository to a Supabase project.
|
||||
@@ -87,8 +137,19 @@ The GitHub app will watch for changes in your repository such as file changes, b
|
||||
These connections will be part of a GitHub workflow that is currently in development.
|
||||
`}
|
||||
integrations={githubIntegrations}
|
||||
onAddConnection={onAddGitHubConnection}
|
||||
onDeleteConnection={onDeleteGitHubConnection}
|
||||
/>
|
||||
<SidePanelVercelProjectLinker
|
||||
isOpen={addPanelOpen === 'VERCEL'}
|
||||
organizationIntegrationId={addPanelIntegrationId}
|
||||
onClose={onCloseAddPanel}
|
||||
/>
|
||||
<SidePanelGitHubRepoLinker
|
||||
isOpen={addPanelOpen === 'GITHUB'}
|
||||
organizationIntegrationId={addPanelIntegrationId}
|
||||
onClose={onCloseAddPanel}
|
||||
/>
|
||||
<SidePanelVercelProjectLinker />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import ProjectLinker from 'components/interfaces/Integrations/ProjectLinker'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { useIntegrationGitHubConnectionsCreateMutation } from 'data/integrations/integrations-github-connections-create-mutation'
|
||||
import { useGitHubReposQuery } from 'data/integrations/integrations-github-repos-query'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { EMPTY_ARR } from 'lib/void'
|
||||
import { SidePanel } from 'ui'
|
||||
|
||||
const GITHUB_ICON = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98 96" className="w-6">
|
||||
<path
|
||||
fill="#ffffff"
|
||||
fillRule="evenodd"
|
||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export type SidePanelGitHubRepoLinkerProps = {
|
||||
isOpen?: boolean
|
||||
onClose?: () => void
|
||||
organizationIntegrationId?: string
|
||||
}
|
||||
|
||||
const SidePanelGitHubRepoLinker = ({
|
||||
isOpen = false,
|
||||
onClose,
|
||||
organizationIntegrationId,
|
||||
}: SidePanelGitHubRepoLinkerProps) => {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const { data: integrationData } = useOrgIntegrationsQuery({
|
||||
orgSlug: selectedOrganization?.slug,
|
||||
})
|
||||
const githubIntegrations = integrationData?.filter(
|
||||
(integration) => integration.integration.name === 'GitHub'
|
||||
) // github
|
||||
|
||||
/**
|
||||
* Find the right integration
|
||||
*
|
||||
* we use the snapshot.organizationIntegrationId which should be set whenever this sidepanel is opened
|
||||
*/
|
||||
const selectedIntegration = githubIntegrations?.find((x) => x.id === organizationIntegrationId)
|
||||
|
||||
/**
|
||||
* Supabase projects available
|
||||
*/
|
||||
const { data: supabaseProjectsData } = useProjectsQuery({
|
||||
enabled: organizationIntegrationId !== undefined,
|
||||
})
|
||||
|
||||
const supabaseProjects = useMemo(
|
||||
() =>
|
||||
supabaseProjectsData
|
||||
?.filter((project) => project.organization_id === selectedOrganization?.id)
|
||||
.map((project) => ({ id: project.id.toString(), name: project.name, ref: project.ref })) ??
|
||||
EMPTY_ARR,
|
||||
[selectedOrganization?.id, supabaseProjectsData]
|
||||
)
|
||||
|
||||
const { data: githubProjectsData } = useGitHubReposQuery(
|
||||
{
|
||||
integrationId: organizationIntegrationId,
|
||||
},
|
||||
{ enabled: organizationIntegrationId !== undefined }
|
||||
)
|
||||
|
||||
const githubRepos = useMemo(
|
||||
() =>
|
||||
githubProjectsData?.map((repo) => ({
|
||||
id: repo.id.toString(),
|
||||
name: repo.full_name,
|
||||
})) ?? EMPTY_ARR,
|
||||
[githubProjectsData]
|
||||
)
|
||||
|
||||
const { mutate: createConnections, isLoading: isCreatingConnection } =
|
||||
useIntegrationGitHubConnectionsCreateMutation({
|
||||
onSuccess() {
|
||||
onClose?.()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
header={'Add GitHub repository'}
|
||||
size="large"
|
||||
visible={isOpen}
|
||||
hideFooter
|
||||
onCancel={() => onClose?.()}
|
||||
>
|
||||
<div className="py-10 flex flex-col gap-6 bg-body h-full">
|
||||
<SidePanel.Content>
|
||||
<Markdown
|
||||
content={`
|
||||
### Choose repository to connect to
|
||||
|
||||
Check the details below before proceeding
|
||||
`}
|
||||
/>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Content className="flex flex-col gap-2">
|
||||
<ProjectLinker
|
||||
organizationIntegrationId={selectedIntegration?.id}
|
||||
foreignProjects={githubRepos}
|
||||
supabaseProjects={supabaseProjects}
|
||||
onCreateConnections={createConnections}
|
||||
installedConnections={selectedIntegration?.connections}
|
||||
isLoading={isCreatingConnection}
|
||||
integrationIcon={GITHUB_ICON}
|
||||
choosePrompt="Choose GitHub Repo"
|
||||
/>
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default SidePanelGitHubRepoLinker
|
||||
@@ -3,7 +3,6 @@ import { Markdown } from 'components/interfaces/Markdown'
|
||||
// import { useIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useStore } from 'hooks'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useGithubConnectionConfigPanelSnapshot } from 'state/github-connection-config-panel'
|
||||
import { IconGitHub, IconSearch, Input, Select, SidePanel } from 'ui'
|
||||
|
||||
// TO DO
|
||||
@@ -13,8 +12,6 @@ const GitHubRepoSelection = () => {
|
||||
const { slug } = useParams()
|
||||
// const { data } = useIntegrationsQuery({ orgSlug: slug })
|
||||
|
||||
const githubConnectionConfigPanelSnapshot = useGithubConnectionConfigPanelSnapshot()
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
header={'Add GitHub repository'}
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
import { useMemo } from 'react'
|
||||
import { keyBy } from 'lodash'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import ProjectLinker from 'components/interfaces/Integrations/ProjectLinker'
|
||||
import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants'
|
||||
import ProjectLinker, { ForeignProject } from 'components/interfaces/Integrations/ProjectLinker'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { vercelIcon } from 'components/to-be-cleaned/ListIcons'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useIntegrationVercelConnectionsCreateMutation } from 'data/integrations/integrations-vercel-connections-create-mutation'
|
||||
import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { EMPTY_ARR } from 'lib/void'
|
||||
import { useGithubConnectionConfigPanelSnapshot } from 'state/github-connection-config-panel'
|
||||
import { SidePanel } from 'ui'
|
||||
|
||||
const SidePanelVercelProjectLinker = () => {
|
||||
const VERCEL_ICON = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 512 512" className="w-6">
|
||||
<path fill-rule="evenodd" d="M256,48,496,464H16Z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export type SidePanelVercelProjectLinkerProps = {
|
||||
isOpen?: boolean
|
||||
onClose?: () => void
|
||||
organizationIntegrationId?: string
|
||||
}
|
||||
|
||||
const SidePanelVercelProjectLinker = ({
|
||||
isOpen = false,
|
||||
onClose,
|
||||
organizationIntegrationId,
|
||||
}: SidePanelVercelProjectLinkerProps) => {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const { data: integrationData } = useOrgIntegrationsQuery({
|
||||
@@ -20,19 +40,12 @@ const SidePanelVercelProjectLinker = () => {
|
||||
(integration) => integration.integration.name === 'Vercel'
|
||||
) // vercel
|
||||
|
||||
const snapshot = useGithubConnectionConfigPanelSnapshot()
|
||||
|
||||
/**
|
||||
* Find the right integration
|
||||
*
|
||||
* we use the snapshot.organizationIntegrationId which should be set whenever this sidepanel is opened
|
||||
*/
|
||||
const selectedIntegration = vercelIntegrations?.find(
|
||||
(x) => x.id === snapshot.organizationIntegrationId
|
||||
)
|
||||
|
||||
const organizationIntegrationId = snapshot.organizationIntegrationId
|
||||
const open = snapshot.open
|
||||
const selectedIntegration = vercelIntegrations?.find((x) => x.id === organizationIntegrationId)
|
||||
|
||||
/**
|
||||
* Supabase projects available
|
||||
@@ -58,14 +71,60 @@ const SidePanelVercelProjectLinker = () => {
|
||||
)
|
||||
|
||||
const vercelProjects = useMemo(() => vercelProjectsData ?? EMPTY_ARR, [vercelProjectsData])
|
||||
const vercelProjectsById = useMemo(() => keyBy(vercelProjects, 'id'), [vercelProjects])
|
||||
|
||||
const getForeignProjectIcon = useCallback(
|
||||
(_project: ForeignProject) => {
|
||||
const project = vercelProjectsById[_project.id]
|
||||
|
||||
return !project?.framework ? (
|
||||
vercelIcon
|
||||
) : (
|
||||
<img
|
||||
src={`${BASE_PATH}/img/icons/frameworks/${project.framework}.svg`}
|
||||
width={21}
|
||||
height={21}
|
||||
alt={`icon`}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[vercelProjectsById]
|
||||
)
|
||||
|
||||
const { mutate: createConnections, isLoading: isCreatingConnection } =
|
||||
useIntegrationVercelConnectionsCreateMutation({
|
||||
onSuccess() {
|
||||
onClose?.()
|
||||
},
|
||||
})
|
||||
|
||||
const onCreateConnections = useCallback(
|
||||
(vars) => {
|
||||
createConnections({
|
||||
...vars,
|
||||
connection: {
|
||||
...vars.connection,
|
||||
metadata: {
|
||||
...vars.connection.metadata,
|
||||
supabaseConfig: {
|
||||
projectEnvVars: {
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
[createConnections]
|
||||
)
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
header={'Add GitHub repository'}
|
||||
size="large"
|
||||
visible={open}
|
||||
visible={isOpen}
|
||||
hideFooter
|
||||
onCancel={() => snapshot.setOpen(false)}
|
||||
onCancel={() => onClose?.()}
|
||||
>
|
||||
<div className="py-10 flex flex-col gap-6 bg-body h-full">
|
||||
<SidePanel.Content>
|
||||
@@ -82,10 +141,24 @@ Check the details below before proceeding
|
||||
organizationIntegrationId={selectedIntegration?.id}
|
||||
foreignProjects={vercelProjects}
|
||||
supabaseProjects={supabaseProjects}
|
||||
onCreateConnections={() => {
|
||||
snapshot.setOpen(false)
|
||||
}}
|
||||
onCreateConnections={onCreateConnections}
|
||||
installedConnections={selectedIntegration?.connections}
|
||||
isLoading={isCreatingConnection}
|
||||
integrationIcon={VERCEL_ICON}
|
||||
getForeignProjectIcon={getForeignProjectIcon}
|
||||
choosePrompt="Choose Vercel Project"
|
||||
/>
|
||||
<Markdown
|
||||
content={`
|
||||
The following environment variables will be added:
|
||||
|
||||
${ENV_VAR_RAW_KEYS.map((x) => {
|
||||
return `
|
||||
\n
|
||||
- \`${x}\`
|
||||
`
|
||||
})}
|
||||
`}
|
||||
/>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Content>
|
||||
|
||||
@@ -20,11 +20,20 @@ import AlertError from 'components/ui/AlertError'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useFlag } from 'hooks'
|
||||
import { useFlag, useProjectByRef } from 'hooks'
|
||||
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
|
||||
import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { SUBSCRIPTION_PANEL_KEYS, useSubscriptionPageStateSnapshot } from 'state/subscription-page'
|
||||
import { Alert, Button, IconChevronRight, IconExternalLink } from 'ui'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
IconAlertCircle,
|
||||
IconChevronRight,
|
||||
IconExternalLink,
|
||||
} from 'ui'
|
||||
import { ComputeInstanceSidePanel, CustomDomainSidePanel, PITRSidePanel } from './'
|
||||
|
||||
const Addons = () => {
|
||||
@@ -34,6 +43,8 @@ const Addons = () => {
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
const { project: selectedProject } = useProjectContext()
|
||||
|
||||
const parentProject = useProjectByRef(selectedProject?.parent_project_ref)
|
||||
const isBranch = parentProject !== undefined
|
||||
const isProjectActive = useIsProjectActive()
|
||||
const allowedPanelValues = ['computeInstance', 'pitr', 'customDomain']
|
||||
if (panel && typeof panel === 'string' && allowedPanelValues.includes(panel)) {
|
||||
@@ -78,6 +89,25 @@ const Addons = () => {
|
||||
|
||||
<ScaffoldDivider />
|
||||
|
||||
{isBranch && (
|
||||
<ScaffoldContainer>
|
||||
<Alert_Shadcn_ variant="default" className="mt-6">
|
||||
<IconAlertCircle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>
|
||||
You are currently on a preview branch of your project
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Updating addons are not available while you're on a preview branch. To manage your
|
||||
addons, you may return to your{' '}
|
||||
<Link passHref href={`/project/${parentProject.ref}/settings/general`}>
|
||||
<a className="text-brand-900">main branch</a>
|
||||
</Link>
|
||||
.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
</ScaffoldContainer>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
@@ -161,7 +191,7 @@ const Addons = () => {
|
||||
type="default"
|
||||
className="mt-2 pointer-events-auto"
|
||||
onClick={() => snap.setPanelKey('computeInstance')}
|
||||
disabled={!isProjectActive || projectUpdateDisabled}
|
||||
disabled={isBranch || !isProjectActive || projectUpdateDisabled}
|
||||
>
|
||||
Change optimized compute
|
||||
</Button>
|
||||
@@ -356,7 +386,7 @@ const Addons = () => {
|
||||
type="default"
|
||||
className="mt-2 pointer-events-auto"
|
||||
onClick={() => snap.setPanelKey('pitr')}
|
||||
disabled={!isProjectActive || projectUpdateDisabled}
|
||||
disabled={isBranch || !isProjectActive || projectUpdateDisabled}
|
||||
>
|
||||
Change point in time recovery
|
||||
</Button>
|
||||
@@ -420,7 +450,7 @@ const Addons = () => {
|
||||
type="default"
|
||||
className="mt-2 pointer-events-auto"
|
||||
onClick={() => snap.setPanelKey('customDomain')}
|
||||
disabled={!isProjectActive || projectUpdateDisabled}
|
||||
disabled={isBranch || !isProjectActive || projectUpdateDisabled}
|
||||
>
|
||||
Change custom domain
|
||||
</Button>
|
||||
|
||||
@@ -34,6 +34,8 @@ const ConnectionPooling = () => {
|
||||
'ignore_startup_parameters',
|
||||
'pool_mode',
|
||||
'pgbouncer_enabled',
|
||||
'max_client_conn',
|
||||
'connectionString',
|
||||
]
|
||||
const bouncerInfo = isSuccess ? pluckObjectFields(formModel, BOUNCER_FIELDS) : {}
|
||||
|
||||
@@ -105,6 +107,8 @@ interface ConfigProps {
|
||||
ignore_startup_parameters: 'string'
|
||||
pool_mode: string
|
||||
pgbouncer_enabled: boolean
|
||||
max_client_conn: number
|
||||
connectionString: string
|
||||
}
|
||||
connectionInfo: {
|
||||
db_host: string
|
||||
@@ -123,11 +127,12 @@ export const PgbouncerConfig: FC<ConfigProps> = ({ projectRef, bouncerInfo, conn
|
||||
'projects'
|
||||
)
|
||||
|
||||
const [updates, setUpdates] = useState<any>({
|
||||
const [updates, setUpdates] = useState({
|
||||
pool_mode: bouncerInfo.pool_mode || 'transaction',
|
||||
default_pool_size: bouncerInfo.default_pool_size || undefined,
|
||||
ignore_startup_parameters: bouncerInfo.ignore_startup_parameters || '',
|
||||
pgbouncer_enabled: bouncerInfo.pgbouncer_enabled,
|
||||
max_client_conn: bouncerInfo.max_client_conn || undefined,
|
||||
})
|
||||
|
||||
const updateConfig = async (updatedConfig: any) => {
|
||||
@@ -174,6 +179,16 @@ export const PgbouncerConfig: FC<ConfigProps> = ({ projectRef, bouncerInfo, conn
|
||||
type: 'string',
|
||||
help: 'Defaults are either blank or "extra_float_digits"',
|
||||
},
|
||||
max_client_conn: {
|
||||
title: 'Max Client Connections',
|
||||
oneOf: [{ type: 'integer' }, { type: 'null' }],
|
||||
help: 'The maximum number of concurrent client connections allowed. Overrides default optimizations; refer to https://supabase.com/docs/guides/platform/custom-postgres-config#pooler-config',
|
||||
},
|
||||
default_pool_size: {
|
||||
title: 'Default Pool Size',
|
||||
oneOf: [{ type: 'integer' }, { type: 'null' }],
|
||||
help: 'The maximum number of connections made to the underlying Postgres cluster, per user+db combination. Overrides default optimizations; refer to https://supabase.com/docs/guides/platform/custom-postgres-config#pooler-config',
|
||||
},
|
||||
},
|
||||
required: ['pool_mode'],
|
||||
type: 'object',
|
||||
@@ -182,7 +197,7 @@ export const PgbouncerConfig: FC<ConfigProps> = ({ projectRef, bouncerInfo, conn
|
||||
return (
|
||||
<div>
|
||||
<SchemaFormPanel
|
||||
title="Connection Pooling"
|
||||
title="Connection Pooling Custom Configuration"
|
||||
schema={formSchema}
|
||||
model={updates}
|
||||
submitLabel="Save"
|
||||
@@ -219,6 +234,10 @@ export const PgbouncerConfig: FC<ConfigProps> = ({ projectRef, bouncerInfo, conn
|
||||
</div>
|
||||
<Divider light />
|
||||
<AutoField name="ignore_startup_parameters" />
|
||||
<Divider light />
|
||||
<AutoField name="max_client_conn" />
|
||||
<Divider light />
|
||||
<AutoField name="default_pool_size" />
|
||||
</>
|
||||
)}
|
||||
<Divider light />
|
||||
@@ -239,11 +258,7 @@ export const PgbouncerConfig: FC<ConfigProps> = ({ projectRef, bouncerInfo, conn
|
||||
copy
|
||||
disabled
|
||||
label="Connection string"
|
||||
value={
|
||||
`postgres://${connectionInfo.db_user}:[YOUR-PASSWORD]@` +
|
||||
`${connectionInfo.db_host}:${connectionInfo.db_port}` +
|
||||
`/${connectionInfo.db_name}`
|
||||
}
|
||||
value={bouncerInfo.connectionString}
|
||||
/>
|
||||
</div>
|
||||
</SchemaFormPanel>
|
||||
|
||||
@@ -69,6 +69,7 @@ const CustomDomainConfig = () => {
|
||||
? 'To configure a custom domain for your project, please upgrade to the Pro plan with the custom domains add-on selected'
|
||||
: 'To configure a custom domain for your project, please enable the add-on'
|
||||
}
|
||||
addon="customDomain"
|
||||
/>
|
||||
) : (
|
||||
<Panel>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import Link from 'next/link'
|
||||
import { Button, Form, IconBarChart2, Input } from 'ui'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
Form,
|
||||
IconAlertCircle,
|
||||
IconBarChart2,
|
||||
Input,
|
||||
} from 'ui'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import {
|
||||
@@ -14,7 +23,7 @@ import {
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useProjectUpdateMutation } from 'data/projects/project-update-mutation'
|
||||
import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks'
|
||||
import { useCheckPermissions, useProjectByRef, useSelectedOrganization, useStore } from 'hooks'
|
||||
import PauseProjectButton from './Infrastructure/PauseProjectButton'
|
||||
import RestartServerButton from './Infrastructure/RestartServerButton'
|
||||
|
||||
@@ -23,6 +32,9 @@ const General = () => {
|
||||
const { project } = useProjectContext()
|
||||
const organization = useSelectedOrganization()
|
||||
|
||||
const parentProject = useProjectByRef(project?.parent_project_ref)
|
||||
const isBranch = parentProject !== undefined
|
||||
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
const formId = 'project-general-settings'
|
||||
const initialValues = { name: project?.name ?? '', ref: project?.ref ?? '' }
|
||||
@@ -41,6 +53,24 @@ const General = () => {
|
||||
return (
|
||||
<div>
|
||||
<FormHeader title="Project Settings" description="" />
|
||||
|
||||
{isBranch && (
|
||||
<Alert_Shadcn_ variant="default" className="mb-6">
|
||||
<IconAlertCircle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>
|
||||
You are currently on a preview branch of your project
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Certain settings are not available while you're on a preview branch. To adjust your
|
||||
project settings, you may return to your{' '}
|
||||
<Link passHref href={`/project/${parentProject.ref}/settings/general`}>
|
||||
<a className="text-brand-900">main branch</a>
|
||||
</Link>
|
||||
.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
|
||||
{project === undefined ? (
|
||||
<GenericSkeletonLoader />
|
||||
) : (
|
||||
@@ -72,7 +102,7 @@ const General = () => {
|
||||
id="name"
|
||||
size="small"
|
||||
label="Project name"
|
||||
disabled={!canUpdateProject}
|
||||
disabled={isBranch || !canUpdateProject}
|
||||
/>
|
||||
<Input copy disabled id="ref" size="small" label="Reference ID" />
|
||||
</FormSectionContent>
|
||||
@@ -82,7 +112,7 @@ const General = () => {
|
||||
}}
|
||||
</Form>
|
||||
)}
|
||||
{isOrgBilling && (
|
||||
{!isBranch && isOrgBilling && (
|
||||
<>
|
||||
<div className="mt-6">
|
||||
<FormPanel>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react'
|
||||
import { Alert, Button, IconEye, IconEyeOff } from 'ui'
|
||||
import DataGrid, { Row, RowRendererProps } from '@supabase/react-data-grid'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Alert, Button, IconClipboard, IconEye, IconEyeOff } from 'ui'
|
||||
|
||||
import LogSelection, { LogSelectionProps } from './LogSelection'
|
||||
import { LogData, QueryType } from './Logs.types'
|
||||
import { isDefaultLogPreviewFormat } from './Logs.utils'
|
||||
import CSVButton from 'components/ui/CSVButton'
|
||||
import { useStore } from 'hooks'
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { isEqual } from 'lodash'
|
||||
import { LogQueryError } from '.'
|
||||
import AuthColumnRenderer from './LogColumnRenderers/AuthColumnRenderer'
|
||||
import DatabaseApiColumnRender from './LogColumnRenderers/DatabaseApiColumnRender'
|
||||
import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresColumnRender'
|
||||
import DefaultPreviewColumnRenderer from './LogColumnRenderers/DefaultPreviewColumnRenderer'
|
||||
import { LogQueryError } from '.'
|
||||
import ResourcesExceededErrorRenderer from './LogsErrorRenderers/ResourcesExceededErrorRenderer'
|
||||
import DefaultErrorRenderer from './LogsErrorRenderers/DefaultErrorRenderer'
|
||||
import FunctionsLogsColumnRender from './LogColumnRenderers/FunctionsLogsColumnRender'
|
||||
import FunctionsEdgeColumnRender from './LogColumnRenderers/FunctionsEdgeColumnRender'
|
||||
import AuthColumnRenderer from './LogColumnRenderers/AuthColumnRenderer'
|
||||
import { isEqual } from 'lodash'
|
||||
import FunctionsLogsColumnRender from './LogColumnRenderers/FunctionsLogsColumnRender'
|
||||
import LogSelection, { LogSelectionProps } from './LogSelection'
|
||||
import { LogData, QueryType } from './Logs.types'
|
||||
import { isDefaultLogPreviewFormat } from './Logs.utils'
|
||||
import DefaultErrorRenderer from './LogsErrorRenderers/DefaultErrorRenderer'
|
||||
import ResourcesExceededErrorRenderer from './LogsErrorRenderers/ResourcesExceededErrorRenderer'
|
||||
|
||||
interface Props {
|
||||
data?: Array<LogData | Object>
|
||||
@@ -64,6 +66,7 @@ const LogTable = ({
|
||||
projectRef,
|
||||
params,
|
||||
}: Props) => {
|
||||
const { ui } = useStore()
|
||||
const [focusedLog, setFocusedLog] = useState<LogData | null>(null)
|
||||
const firstRow: LogData | undefined = data?.[0] as LogData
|
||||
const columnNames = Object.keys(data[0] || {})
|
||||
@@ -170,6 +173,12 @@ const LogTable = ({
|
||||
[]
|
||||
)
|
||||
|
||||
const copyResultsToClipboard = () => {
|
||||
copyToClipboard(stringData, () => {
|
||||
ui.setNotification({ category: 'success', message: 'Results copied to clipboard.' })
|
||||
})
|
||||
}
|
||||
|
||||
const LogsExplorerTableHeader = () => (
|
||||
<div className="flex w-full items-center justify-between rounded-tl rounded-tr border-t border-l border-r bg-scale-100 px-5 py-2 dark:bg-scale-300">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -192,6 +201,9 @@ const LogTable = ({
|
||||
Histogram
|
||||
</Button>
|
||||
)}
|
||||
<Button type="default" icon={<IconClipboard />} onClick={copyResultsToClipboard}>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
<CSVButton data={data}>Download</CSVButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useFlag } from 'hooks'
|
||||
import { isUndefined } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
import { Badge, IconArrowUpRight, IconLogOut, Menu } from 'ui'
|
||||
import LayoutHeader from '../ProjectLayout/LayoutHeader'
|
||||
import { SidebarLink, SidebarSection } from './AccountLayout.types'
|
||||
@@ -104,11 +104,7 @@ interface SectionWithHeadersProps {
|
||||
subitemsParentKey?: number
|
||||
}
|
||||
|
||||
const SectionWithHeaders: FC<SectionWithHeadersProps> = ({
|
||||
section,
|
||||
subitems,
|
||||
subitemsParentKey,
|
||||
}) => (
|
||||
const SectionWithHeaders = ({ section, subitems, subitemsParentKey }: SectionWithHeadersProps) => (
|
||||
<div key={section.heading} className="border-b py-5 px-6 border-scale-500">
|
||||
{section.heading && <Menu.Group title={section.heading} />}
|
||||
{section.versionLabel && (
|
||||
@@ -131,7 +127,7 @@ interface SidebarItemProps {
|
||||
subitemsParentKey?: number
|
||||
}
|
||||
|
||||
const SidebarItem: FC<SidebarItemProps> = ({ links, subitems, subitemsParentKey }) => {
|
||||
const SidebarItem = ({ links, subitems, subitemsParentKey }: SidebarItemProps) => {
|
||||
return (
|
||||
<ul className="space-y-1">
|
||||
{links.map((link, i: number) => {
|
||||
@@ -174,7 +170,7 @@ interface SidebarLinkProps extends SidebarLink {
|
||||
isSubitem?: boolean
|
||||
}
|
||||
|
||||
const SidebarLinkItem: FC<SidebarLinkProps> = ({
|
||||
const SidebarLinkItem = ({
|
||||
id,
|
||||
label,
|
||||
href,
|
||||
@@ -182,7 +178,7 @@ const SidebarLinkItem: FC<SidebarLinkProps> = ({
|
||||
isSubitem,
|
||||
isExternal,
|
||||
onClick,
|
||||
}) => {
|
||||
}: SidebarLinkProps) => {
|
||||
if (isUndefined(href)) {
|
||||
let icon
|
||||
if (isExternal) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { useFlag, useSelectedOrganization } from 'hooks'
|
||||
import FeedbackDropdown from '../ProjectLayout/LayoutHeader/FeedbackDropdown'
|
||||
import HelpPopover from '../ProjectLayout/LayoutHeader/HelpPopover'
|
||||
import NotificationsPopover from '../ProjectLayout/LayoutHeader/NotificationsPopover'
|
||||
@@ -10,11 +10,13 @@ import OrganizationDropdown from './OrganizationDropdown'
|
||||
import ProjectDropdown from './ProjectDropdown'
|
||||
import SettingsButton from './SettingsButton'
|
||||
import UserSettingsDropdown from './UserSettingsDropdown'
|
||||
import BranchDropdown from './BranchDropdown'
|
||||
|
||||
const AppHeader = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const organization = useSelectedOrganization()
|
||||
const enableBranchManagement = useFlag('branchManagement')
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 py-1 bg-scale-200 border-b">
|
||||
@@ -30,6 +32,7 @@ const AppHeader = () => {
|
||||
</Link>
|
||||
<OrganizationDropdown />
|
||||
{ref !== undefined && <ProjectDropdown />}
|
||||
{ref !== undefined && enableBranchManagement && <BranchDropdown />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { Branch, useBranchesQuery } from 'data/branches/branches-query'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
IconAlertCircle,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
IconGitBranch,
|
||||
IconPlus,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
import { useSelectedProject } from 'hooks'
|
||||
import { sanitizeRoute } from './ProjectDropdown'
|
||||
|
||||
const BranchLink = ({
|
||||
branch,
|
||||
isSelected,
|
||||
setOpen,
|
||||
}: {
|
||||
branch: Branch
|
||||
isSelected: boolean
|
||||
setOpen: (value: boolean) => void
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const sanitizedRoute = sanitizeRoute(router.route, router.query)
|
||||
const href =
|
||||
sanitizedRoute?.replace('[ref]', branch.project_ref) ?? `/project/${branch.project_ref}`
|
||||
|
||||
return (
|
||||
<Link passHref href={href}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
value={branch.name}
|
||||
className="cursor-pointer w-full flex items-center justify-between"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
router.push(href)
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<a>
|
||||
{branch.name}
|
||||
{isSelected && <IconCheck />}
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const BranchDropdown = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const projectDetails = useSelectedProject()
|
||||
const branchNameRef = useRef<HTMLAnchorElement>(null)
|
||||
|
||||
const isBranch = projectDetails?.parent_project_ref !== undefined
|
||||
const projectRef =
|
||||
projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
|
||||
const { data: branches, isLoading, isError, isSuccess } = useBranchesQuery({ projectRef })
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const popoverOffset = (branchNameRef.current?.offsetWidth ?? 0) + 12
|
||||
const selectedBranch = branches?.find((branch) => branch.project_ref === ref)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <ShimmeringLoader className="w-[90px]" />}
|
||||
|
||||
{isError && (
|
||||
<div className="flex items-center space-x-2 text-amber-900">
|
||||
<IconAlertCircle size={16} strokeWidth={2} />
|
||||
<p className="text-sm">Failed to load branches</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSuccess && branches.length > 0 && (
|
||||
<div className="flex items-center space-x-2 px-2">
|
||||
<Link passHref href={`/project/${ref}`}>
|
||||
<a ref={branchNameRef} className="flex items-center space-x-2 text-sm">
|
||||
{selectedBranch?.name}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Link passHref href={`/project/${ref}/branches`}>
|
||||
<a ref={branchNameRef} className="flex items-center space-x-2">
|
||||
{selectedBranch?.is_default ? (
|
||||
<Badge color="amber">Production</Badge>
|
||||
) : (
|
||||
<Badge color="green">Preview Branch</Badge>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={<IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} />}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
style={{ marginLeft: `-${popoverOffset}px` }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Find branch..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No branches found</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{branches?.map((branch) => (
|
||||
<BranchLink
|
||||
key={branch.id}
|
||||
branch={branch}
|
||||
isSelected={branch.id === selectedBranch?.id}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
<CommandGroup_Shadcn_ className="border-t">
|
||||
<Link passHref href={`/project/${ref}/branches`}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
className="cursor-pointer flex items-center space-x-2 w-full"
|
||||
onSelect={(e) => {
|
||||
setOpen(false)
|
||||
router.push(`/project/${ref}/branches`)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
<IconGitBranch size={14} strokeWidth={1.5} />
|
||||
<p>Manage branches</p>
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BranchDropdown
|
||||
@@ -76,8 +76,6 @@ const OrganizationDropdown = () => {
|
||||
{organizations?.map((org) => {
|
||||
const href = router.pathname.includes('[slug]')
|
||||
? router.pathname.replace('[slug]', org.slug)
|
||||
: router.pathname.includes('[ref]/settings')
|
||||
? `/org/${org.slug}/general`
|
||||
: `/org/${org.slug}`
|
||||
return (
|
||||
<Link passHref href={href} key={org.slug}>
|
||||
|
||||
@@ -106,6 +106,11 @@ const ProjectDropdown = () => {
|
||||
const projectNameRef = useRef<HTMLAnchorElement>(null)
|
||||
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
|
||||
|
||||
const isBranch = projectDetails?.parent_project_ref !== undefined
|
||||
const parentProject = isBranch
|
||||
? allProjects?.find((project) => project.ref === projectDetails.parent_project_ref)
|
||||
: undefined
|
||||
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
const { data: subscription, isSuccess } = useProjectSubscriptionV2Query(
|
||||
{ projectRef: ref },
|
||||
@@ -115,7 +120,9 @@ const ProjectDropdown = () => {
|
||||
?.filter((x) => x.status !== PROJECT_STATUS.INACTIVE)
|
||||
.filter((x) => x.organization_id === selectedOrganization?.id)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const selectedProject = projectDetails || projects?.find((project) => project.ref === ref)
|
||||
const selectedProject = isBranch
|
||||
? parentProject
|
||||
: projectDetails || projects?.find((project) => project.ref === ref)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const popoverOffset = (projectNameRef.current?.offsetWidth ?? 0) + 12
|
||||
@@ -126,7 +133,7 @@ const ProjectDropdown = () => {
|
||||
|
||||
return IS_PLATFORM ? (
|
||||
<div className="flex items-center space-x-2 px-2">
|
||||
<Link href={`/project/${selectedProject?.ref}`}>
|
||||
<Link href={`/project/${ref}`}>
|
||||
<a ref={projectNameRef} className="flex items-center space-x-2">
|
||||
<p className="text-sm">{selectedProject?.name}</p>
|
||||
{isSuccess && !isOrgBilling && <Badge color="slate">{subscription?.plan.name}</Badge>}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
DropdownMenuCheckboxItem_Shadcn_,
|
||||
DropdownMenuContent_Shadcn_,
|
||||
DropdownMenuGroup_Shadcn_,
|
||||
DropdownMenuItem_Shadcn_,
|
||||
@@ -10,12 +9,8 @@ import {
|
||||
DropdownMenuRadioItem_Shadcn_,
|
||||
DropdownMenuSeparator_Shadcn_,
|
||||
DropdownMenuShortcut_Shadcn_,
|
||||
DropdownMenuSubContent_Shadcn_,
|
||||
DropdownMenuSubTrigger_Shadcn_,
|
||||
DropdownMenuSub_Shadcn_,
|
||||
DropdownMenuTrigger_Shadcn_,
|
||||
DropdownMenu_Shadcn_,
|
||||
IconCheck,
|
||||
IconUser,
|
||||
useCommandMenu,
|
||||
} from 'ui'
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface AuthLayoutProps {
|
||||
}
|
||||
|
||||
const AuthLayout = ({ title, children }: PropsWithChildren<AuthLayoutProps>) => {
|
||||
const { ui, meta } = useStore()
|
||||
const { ui, meta, authConfig } = useStore()
|
||||
const { isInitialized, isLoading, error } = meta.tables
|
||||
const { ref: projectRef = 'default' } = useParams()
|
||||
|
||||
@@ -29,6 +29,7 @@ const AuthLayout = ({ title, children }: PropsWithChildren<AuthLayoutProps>) =>
|
||||
meta.tables.load()
|
||||
meta.roles.load()
|
||||
meta.schemas.load()
|
||||
authConfig.load()
|
||||
}
|
||||
}, [ui.selectedProjectRef])
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import IntegrationWindowLayout from './IntegrationWindowLayout'
|
||||
|
||||
const GITHUB_ICON = (
|
||||
<div className="bg-black shadow rounded p-1 w-8 h-8 flex justify-center items-center">
|
||||
<img
|
||||
src={`${BASE_PATH}/img/icons/github-icon-dark.svg`}
|
||||
alt="GitHub Icon"
|
||||
className="w-[18px]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
const GitHubIntegrationWindowLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<IntegrationWindowLayout title="Supabase + GitHub Integration" integrationIcon={GITHUB_ICON}>
|
||||
{children}
|
||||
</IntegrationWindowLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitHubIntegrationWindowLayout
|
||||
@@ -1,20 +1,27 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { IconChevronRight, IconX, cn } from 'ui'
|
||||
import { withAuth, useFlag } from 'hooks'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import React from 'react'
|
||||
import { ScaffoldContainer } from './Scaffold'
|
||||
import { useParams } from 'common'
|
||||
import { useFlag, withAuth } from 'hooks'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { PropsWithChildren, ReactNode, forwardRef } from 'react'
|
||||
import { IconX, cn } from 'ui'
|
||||
import { ScaffoldContainer } from '../Scaffold'
|
||||
|
||||
const IntegrationWindowLayout: FC<any> = ({ organization, project, children }) => {
|
||||
export type IntegrationWindowLayoutProps = {
|
||||
title: string
|
||||
integrationIcon: ReactNode
|
||||
}
|
||||
|
||||
const IntegrationWindowLayout = ({
|
||||
title,
|
||||
integrationIcon,
|
||||
children,
|
||||
}: PropsWithChildren<IntegrationWindowLayoutProps>) => {
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col" style={{ height: maxHeight, maxHeight }}>
|
||||
<Header organization={organization} project={project} />
|
||||
<Header title={title} integrationIcon={integrationIcon} />
|
||||
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -22,18 +29,16 @@ const IntegrationWindowLayout: FC<any> = ({ organization, project, children }) =
|
||||
|
||||
const INTEGRATION_LAYOUT_MAX_WIDTH = '' // 'max-w-[720px]'
|
||||
|
||||
export default withAuth(observer(IntegrationWindowLayout))
|
||||
export default withAuth(IntegrationWindowLayout)
|
||||
|
||||
export const IntegrationWindowLayoutWithoutAuth = observer(IntegrationWindowLayout)
|
||||
export const IntegrationWindowLayoutWithoutAuth = IntegrationWindowLayout
|
||||
|
||||
const Header: FC<any> = ({ organization, project }) => {
|
||||
let stepNumber = organization ? 1 : project ? 2 : 0
|
||||
const { externalId } = useParams()
|
||||
|
||||
const title = externalId
|
||||
? 'Supabase + Vercel Deploy Button'
|
||||
: 'Supabase + Vercel Integration Marketplace Connector'
|
||||
export type HeaderProps = {
|
||||
title: string
|
||||
integrationIcon: ReactNode
|
||||
}
|
||||
|
||||
const Header = ({ title, integrationIcon }: HeaderProps) => {
|
||||
return (
|
||||
<div className="bg">
|
||||
<ScaffoldContainer className={cn('py-3', INTEGRATION_LAYOUT_MAX_WIDTH)}>
|
||||
@@ -43,16 +48,7 @@ const Header: FC<any> = ({ organization, project }) => {
|
||||
<img src={`${BASE_PATH}/img/supabase-logo.svg`} alt="Supabase" className="w-4" />
|
||||
</div>
|
||||
<IconX className="text-scale-800" strokeWidth={2} size={16} />
|
||||
<div className="bg-black shadow rounded p-1 w-8 h-8 flex justify-center items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="white"
|
||||
viewBox="0 0 512 512"
|
||||
className="w-4"
|
||||
>
|
||||
<path fillRule="evenodd" d="M256,48,496,464H16Z" />
|
||||
</svg>
|
||||
</div>
|
||||
{integrationIcon}
|
||||
</div>
|
||||
<span className="text-sm" title={title}>
|
||||
{title}
|
||||
@@ -66,7 +62,7 @@ const Header: FC<any> = ({ organization, project }) => {
|
||||
const maxWidthClasses = 'mx-auto w-full max-w-[1600px]'
|
||||
const paddingClasses = 'px-6 lg:px-14 xl:px-28 2xl:px-32'
|
||||
|
||||
const IntegrationScaffoldContainer = React.forwardRef<
|
||||
const IntegrationScaffoldContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
@@ -0,0 +1,54 @@
|
||||
import { IntegrationsData } from 'data/integrations/integrations-query'
|
||||
import { IntegrationName } from 'data/integrations/integrations.types'
|
||||
import { Organization } from 'types'
|
||||
|
||||
export function getHasInstalledObject({
|
||||
integrationName,
|
||||
integrationData,
|
||||
organizationsData,
|
||||
installationId,
|
||||
}: {
|
||||
integrationName: IntegrationName
|
||||
integrationData: IntegrationsData
|
||||
organizationsData: Organization[]
|
||||
installationId?: string | number
|
||||
}): { [orgSlug: string]: boolean } {
|
||||
if (integrationName === 'Vercel') {
|
||||
return Object.fromEntries(
|
||||
organizationsData
|
||||
.map((org) => [
|
||||
org.slug,
|
||||
Boolean(
|
||||
integrationData.find(
|
||||
(integration) =>
|
||||
integration.organization.slug === org.slug &&
|
||||
integration.integration.name === 'Vercel'
|
||||
)
|
||||
),
|
||||
])
|
||||
.filter(([, v]) => Boolean(v))
|
||||
)
|
||||
}
|
||||
|
||||
if (integrationName === 'GitHub') {
|
||||
return Object.fromEntries(
|
||||
organizationsData
|
||||
.map((org) => [
|
||||
org.slug,
|
||||
Boolean(
|
||||
integrationData.find(
|
||||
(integration) =>
|
||||
integration.organization.slug === org.slug &&
|
||||
integration.integration.name === 'GitHub' &&
|
||||
integration.metadata !== undefined &&
|
||||
'installation_id' in integration.metadata &&
|
||||
String(integration.metadata.installation_id) === String(installationId)
|
||||
)
|
||||
),
|
||||
])
|
||||
.filter(([, v]) => Boolean(v))
|
||||
)
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import IntegrationWindowLayout from './IntegrationWindowLayout'
|
||||
|
||||
const VERCEL_ICON = (
|
||||
<div className="bg-black shadow rounded p-1 w-8 h-8 flex justify-center items-center">
|
||||
<img src={`${BASE_PATH}/img/icons/vercel-icon.svg`} alt="Vercel Icon" className="w-4" />
|
||||
</div>
|
||||
)
|
||||
|
||||
const VercelIntegrationWindowLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { externalId } = useParams()
|
||||
|
||||
const title = externalId
|
||||
? 'Supabase + Vercel Deploy Button'
|
||||
: 'Supabase + Vercel Integration Marketplace Connector'
|
||||
|
||||
return (
|
||||
<IntegrationWindowLayout title={title} integrationIcon={VERCEL_ICON}>
|
||||
{children}
|
||||
</IntegrationWindowLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default VercelIntegrationWindowLayout
|
||||
@@ -1,15 +1,14 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { Badge, Button, Dropdown, IconCode, IconPlus, Popover } from 'ui'
|
||||
import { Button, Dropdown, IconPlus, Popover } from 'ui'
|
||||
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { Organization, Project } from 'types'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
|
||||
// [Fran] the idea is to let users change projects without losing the current page,
|
||||
// but at the same time we need to redirect correctly between urls that might be
|
||||
@@ -73,20 +72,14 @@ const ProjectLink = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ProjectDropdown = ({ alt }: { alt?: boolean }) => {
|
||||
const ProjectDropdown = () => {
|
||||
const selectedProject = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
|
||||
const { data: allOrganizations } = useOrganizationsQuery()
|
||||
const selectedOrganizationSlug = selectedOrganization?.slug
|
||||
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
const { data: subscription, isSuccess } = useProjectSubscriptionV2Query(
|
||||
{ projectRef: selectedProject?.ref },
|
||||
{ enabled: alt && !isOrgBilling }
|
||||
)
|
||||
|
||||
if (isLoadingProjects && alt) {
|
||||
if (isLoadingProjects) {
|
||||
return <ShimmeringLoader className="w-[90px]" />
|
||||
}
|
||||
|
||||
@@ -112,23 +105,13 @@ const ProjectDropdown = ({ alt }: { alt?: boolean }) => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
iconRight={
|
||||
alt ? <IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} /> : null
|
||||
}
|
||||
>
|
||||
<Button type="text">
|
||||
<span className="text-sm">{selectedProject?.name}</span>
|
||||
{alt && isSuccess && !isOrgBilling && (
|
||||
<Badge color="slate" className="ml-2">
|
||||
{subscription?.plan.name}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button type="text">
|
||||
<span className="text-sm">{selectedProject?.name}</span>
|
||||
<span className="text-xs">{selectedProject?.name}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ const NavigationBar = ({}) => {
|
||||
].join(' ')}
|
||||
>
|
||||
<ul className="flex flex-col space-y-2">
|
||||
{!navLayoutV2 && (
|
||||
<Link href="/projects">
|
||||
{(!navLayoutV2 || !IS_PLATFORM) && (
|
||||
<Link href={IS_PLATFORM ? '/projects' : `/project/${projectRef}`}>
|
||||
<a className="block">
|
||||
<img
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
|
||||
@@ -33,7 +33,9 @@ export const generateToolRoutes = (ref?: string, project?: ProjectBase): Route[]
|
||||
preProcessor={(code) => code.replace(/svg/, 'svg class="m-auto text-color-inherit"')}
|
||||
/>
|
||||
),
|
||||
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/sql`),
|
||||
link: !IS_PLATFORM
|
||||
? `/project/${ref}/sql/1`
|
||||
: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/sql`),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Fragment, PropsWithChildren, ReactNode } from 'react'
|
||||
import { useParams } from 'common/hooks'
|
||||
import Connecting from 'components/ui/Loading'
|
||||
import { useFlag, useSelectedOrganization, useSelectedProject, withAuth } from 'hooks'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import { PROJECT_STATUS, IS_PLATFORM } from 'lib/constants'
|
||||
import BuildingState from './BuildingState'
|
||||
import ConnectingState from './ConnectingState'
|
||||
import LayoutHeader from './LayoutHeader'
|
||||
@@ -29,6 +29,8 @@ const routesToIgnoreProjectDetailsRequest = [
|
||||
'/project/[ref]/settings/billing/invoices',
|
||||
]
|
||||
|
||||
const routesToIgnoreDBConnection = ['/project/[ref]/branches']
|
||||
|
||||
const routesToIgnorePostgrestConnection = [
|
||||
'/project/[ref]/reports',
|
||||
'/project/[ref]/settings/general',
|
||||
@@ -101,7 +103,7 @@ const ProjectLayout = ({
|
||||
)}
|
||||
|
||||
<main className="flex flex-col flex-1 w-full overflow-x-hidden">
|
||||
{!navLayoutV2 && !hideHeader && <LayoutHeader />}
|
||||
{!navLayoutV2 && !hideHeader && IS_PLATFORM && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
@@ -161,8 +163,9 @@ const ContentWrapper = ({ isLoading, children }: ContentWrapperProps) => {
|
||||
const router = useRouter()
|
||||
|
||||
const requiresDbConnection: boolean =
|
||||
!router.pathname.includes('/project/[ref]/settings') ||
|
||||
router.pathname.includes('/project/[ref]/settings/vault')
|
||||
(!router.pathname.includes('/project/[ref]/settings') &&
|
||||
!routesToIgnoreDBConnection.includes(router.pathname)) ||
|
||||
router.pathname === '/project/[ref]/settings/vault'
|
||||
const requiresPostgrestConnection = !routesToIgnorePostgrestConnection.includes(router.pathname)
|
||||
const requiresProjectDetails = !routesToIgnoreProjectDetailsRequest.includes(router.pathname)
|
||||
|
||||
@@ -236,7 +239,7 @@ export const ProjectLayoutNonBlocking = ({
|
||||
)}
|
||||
|
||||
<main className="flex w-full flex-1 flex-col overflow-x-hidden">
|
||||
{!navLayoutV2 && !hideHeader && <LayoutHeader />}
|
||||
{!navLayoutV2 && !hideHeader && IS_PLATFORM && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
|
||||
import { IntegrationName } from 'data/integrations/integrations.types'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useState } from 'react'
|
||||
import { Organization } from 'types'
|
||||
import { Badge, IconHexagon, Listbox } from 'ui'
|
||||
|
||||
export interface OrganizationPickerProps {
|
||||
label?: string
|
||||
onSelectedOrgChange?: (org: Organization) => void
|
||||
integrationName: IntegrationName
|
||||
}
|
||||
|
||||
const OrganizationPicker = ({
|
||||
label = 'Choose an organization',
|
||||
onSelectedOrgChange,
|
||||
integrationName,
|
||||
}: OrganizationPickerProps) => {
|
||||
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
|
||||
|
||||
/**
|
||||
* array of integrations installed on all
|
||||
*/
|
||||
const { data: integrationData, isLoading: integrationDataLoading } = useIntegrationsQuery()
|
||||
|
||||
/**
|
||||
* filter integrations to match integrationName
|
||||
*/
|
||||
const vercelIntegrationsInstalled = integrationData?.filter(
|
||||
(integration) => integration.integration.name === 'Vercel'
|
||||
)
|
||||
|
||||
const { data, isLoading } = useOrganizationsQuery({
|
||||
onSuccess(organizations) {
|
||||
const firstOrg = organizations?.[0]
|
||||
if (firstOrg && selectedOrg === null) {
|
||||
setSelectedOrg(firstOrg)
|
||||
onSelectedOrgChange?.(firstOrg)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function _onSelectedOrgChange(slug: string) {
|
||||
const org = data?.find((org) => org.slug === slug)
|
||||
|
||||
if (org) {
|
||||
setSelectedOrg(org)
|
||||
onSelectedOrgChange?.(org)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading || integrationDataLoading) {
|
||||
return (
|
||||
<Listbox label={label} value="loading" disabled>
|
||||
<Listbox.Option key="loading" value="loading" label="Loading...">
|
||||
Loading...
|
||||
</Listbox.Option>
|
||||
</Listbox>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization type with `installationInstalled` added
|
||||
*/
|
||||
interface OrganizationsResponseWithInstalledData extends Organization {
|
||||
installationInstalled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A flat array of org slugs that have integration installed
|
||||
*/
|
||||
const flatInstalledConnectionsIds =
|
||||
integrationData && integrationData.length > 0
|
||||
? integrationData?.map((x) => x.organization.slug)
|
||||
: []
|
||||
|
||||
/**
|
||||
* Organizations with extra `installationInstalled` attribute
|
||||
* Used to show label/badge and allow/disallow installing
|
||||
*/
|
||||
const organizationsWithInstalledData: OrganizationsResponseWithInstalledData[] = data
|
||||
? data.map((org) => {
|
||||
return {
|
||||
...org,
|
||||
installationInstalled: !flatInstalledConnectionsIds.includes(org.slug) ? true : false,
|
||||
}
|
||||
})
|
||||
: []
|
||||
|
||||
return (
|
||||
<Listbox label={label} value={selectedOrg?.slug} onChange={_onSelectedOrgChange}>
|
||||
{organizationsWithInstalledData?.map((org) => {
|
||||
const label = (
|
||||
<div className="flex gap-3 items-center">
|
||||
{org.name}
|
||||
{org.installationInstalled && <Badge color="scale">Integration Installed</Badge>}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={org.id}
|
||||
value={org.slug}
|
||||
// @ts-expect-error
|
||||
label={label}
|
||||
addOnBefore={({ active, selected }: any) => <IconHexagon />}
|
||||
>
|
||||
{label}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrganizationPicker
|
||||
@@ -12,9 +12,10 @@ interface Props {
|
||||
primaryText: string
|
||||
projectRef: string
|
||||
secondaryText: string
|
||||
addon?: 'pitr' | 'customDomain' | 'computeInstance'
|
||||
}
|
||||
|
||||
const UpgradeToPro: FC<Props> = ({ icon, primaryText, projectRef, secondaryText }) => {
|
||||
const UpgradeToPro: FC<Props> = ({ icon, primaryText, projectRef, secondaryText, addon }) => {
|
||||
const { data: subscription } = useProjectSubscriptionV2Query({ projectRef })
|
||||
const plan = subscription?.plan?.id
|
||||
|
||||
@@ -45,8 +46,8 @@ const UpgradeToPro: FC<Props> = ({ icon, primaryText, projectRef, secondaryText
|
||||
<Tooltip.Trigger>
|
||||
<Button type="primary" disabled={!canUpdateSubscription || projectUpdateDisabled}>
|
||||
<Link
|
||||
href={`/project/${projectRef}/settings/billing/subscription${
|
||||
plan === 'free' ? '?panel=subscriptionPlan' : ''
|
||||
href={`/project/${projectRef}/settings/billing/subscription?panel=${
|
||||
plan === 'free' ? 'subscriptionPlan' : addon || 'subscriptionPlan'
|
||||
}`}
|
||||
>
|
||||
<a>{plan === 'free' ? 'Upgrade to Pro' : 'Enable Addon'}</a>
|
||||
|
||||
@@ -72,7 +72,7 @@ return (
|
||||
|
||||
### Mutations
|
||||
|
||||
The parameter `onError` can be passed when intializing the mutation, where an appropriate UI behavior should be triggered (usually a toast will be fine). If `onError` is not provided, we will then default to a toast message that will be called from within the mutation itself which should handle most cases. However, if there is a specific way that the error should be handled, then this should be passed via the `onError` parameter. (There's no need to repeat the error handling if the default behavior is sufficient)
|
||||
The parameter `onError` can be passed when initializing the mutation, where an appropriate UI behavior should be triggered (usually a toast will be fine). If `onError` is not provided, we will then default to a toast message that will be called from within the mutation itself which should handle most cases. However, if there is a specific way that the error should be handled, then this should be passed via the `onError` parameter. (There's no need to repeat the error handling if the default behavior is sufficient)
|
||||
|
||||
We should also aim to convert most of the `mutateAsync` to `mutate`, otherwise we'd need to wrap all the `mutateAsync` calls in try catches just to ensure that the client doesn't crash when the request throws an error in the mutation. There's currently a mix due to the Form component that we're using, in particular when a method from the Form component such as `resetForm` needs to be called after the request is completed successfully.
|
||||
|
||||
@@ -88,7 +88,7 @@ const { mutate: someAction } = useMutation({
|
||||
|
||||
const onConfirm = async () => {
|
||||
// Assuming that your mutation needs a URL param like project ref
|
||||
// This check is just to satistfy the linting - there's an implicit assumption that
|
||||
// This check is just to satisfy the linting - there's an implicit assumption that
|
||||
// projectRef here will definitely be available since its obtained from the URL
|
||||
if (!projectRef) return console.error('Project ref is required')
|
||||
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get, isResponseOk } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get } from 'data/fetchers'
|
||||
import { useCallback } from 'react'
|
||||
import { resourceKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type ResourcesVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export type ResourcesResponse = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export async function getResources({ projectRef }: ResourcesVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
const response = await get<ResourcesResponse[]>(`${API_URL}/projects/${projectRef}/resources`, {
|
||||
// Use any of the API endpoints as the parameter here, TS will check if its valid - "branches" here is just an example
|
||||
const { data, error } = await get(`/v1/projects/{ref}/branches`, {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
if (!isResponseOk(response)) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
return data
|
||||
}
|
||||
|
||||
export type ResourcesData = Awaited<ReturnType<typeof getResources>>
|
||||
export type ResourcesError = unknown
|
||||
export type ResourcesError = ResponseError
|
||||
|
||||
export const useResourcesQuery = <TData = ResourcesData>(
|
||||
{ projectRef }: ResourcesVariables,
|
||||
@@ -43,6 +36,7 @@ export const useResourcesQuery = <TData = ResourcesData>(
|
||||
|
||||
/**
|
||||
* useResourcesPrefetch is used for prefetching data. For example, starting a query loading before a page is navigated to.
|
||||
* Feel free to omit if not required
|
||||
*
|
||||
* @example
|
||||
* const prefetch = useResourcesPrefetch({ projectRef })
|
||||
|
||||
@@ -396,6 +396,8 @@ export interface paths {
|
||||
"/platform/projects/{ref}/content": {
|
||||
/** Gets project's content */
|
||||
get: operations["ContentController_getContent"];
|
||||
/** Updates project's content */
|
||||
put: operations["ContentController_updateWholeContent"];
|
||||
/** Creates project's content */
|
||||
post: operations["ContentController_createContent"];
|
||||
/** Deletes project's content */
|
||||
@@ -712,6 +714,30 @@ export interface paths {
|
||||
/** Deletes vercel project connection */
|
||||
delete: operations["VercelConnectionsController_deleteVercelConnection"];
|
||||
};
|
||||
"/platform/integrations/github": {
|
||||
/** Create github integration */
|
||||
post: operations["GitHubIntegrationController_createGitHubIntegration"];
|
||||
};
|
||||
"/platform/integrations/github/connections/{organization_integration_id}": {
|
||||
/** Gets installed github project connections for the given organization integration */
|
||||
get: operations["GitHubConnectionsController_getGitHubConnections"];
|
||||
};
|
||||
"/platform/integrations/github/connections": {
|
||||
/** Connects a GitHub project to a supabase project */
|
||||
post: operations["GitHubConnectionsController_createGitHubConnection"];
|
||||
};
|
||||
"/platform/integrations/github/connections/{connection_id}": {
|
||||
/** Deletes github project connection */
|
||||
delete: operations["GitHubConnectionsController_deleteGitHubConnection"];
|
||||
};
|
||||
"/platform/integrations/github/repos/{organization_integration_id}": {
|
||||
/** Gets github repos for the given organization */
|
||||
get: operations["GitHubRepoController_getRepos"];
|
||||
};
|
||||
"/platform/integrations/github/branches/{organization_integration_id}/{repo_owner}/{repo_name}": {
|
||||
/** Gets github branches for a given repo */
|
||||
get: operations["GitHubBranchController_getBranches"];
|
||||
};
|
||||
"/system/auth/{ref}/templates/{template}": {
|
||||
/** Gets GoTrue template */
|
||||
get: operations["AuthTemplateController_getTemplate"];
|
||||
@@ -792,6 +818,10 @@ export interface paths {
|
||||
/** Processes Vercel event */
|
||||
post: operations["VercelWebhooksController_processEvent"];
|
||||
};
|
||||
"/system/integrations/github/webhooks": {
|
||||
/** Processes GitHub event */
|
||||
post: operations["GitHubWebhooksController_processEvent"];
|
||||
};
|
||||
"/system/stripe/webhooks-v2": {
|
||||
/** Processes Stripe event */
|
||||
post: operations["StripeWebhooksController_processEvent"];
|
||||
@@ -1056,6 +1086,8 @@ export interface paths {
|
||||
"/v0/projects/{ref}/content": {
|
||||
/** Gets project's content */
|
||||
get: operations["ContentController_getContent"];
|
||||
/** Updates project's content */
|
||||
put: operations["ContentController_updateWholeContent"];
|
||||
/** Creates project's content */
|
||||
post: operations["ContentController_createContent"];
|
||||
/** Deletes project's content */
|
||||
@@ -1247,7 +1279,7 @@ export interface paths {
|
||||
/** Deletes objects */
|
||||
delete: operations["StorageObjectsController_deleteObjects"];
|
||||
};
|
||||
"/v1/branch/{branch_id}": {
|
||||
"/v1/branches/{branch_id}": {
|
||||
/**
|
||||
* Get database branch config
|
||||
* @description Fetches configurations of the specified database branch
|
||||
@@ -1339,7 +1371,7 @@ export interface paths {
|
||||
/** Updates project's postgrest config */
|
||||
patch: operations["PostgrestConfigController_updatePostgRESTConfig"];
|
||||
};
|
||||
"/v1/projects/{ref}/query": {
|
||||
"/v1/projects/{ref}/database/query": {
|
||||
/** Run sql query */
|
||||
post: operations["QueryController_runQuery"];
|
||||
};
|
||||
@@ -1485,7 +1517,6 @@ export interface paths {
|
||||
post: operations["OAuthAppsController_createOAuthApp"];
|
||||
};
|
||||
"/v1/organizations/{slug}/oauth/apps/{id}": {
|
||||
get: operations["OAuthAppsController_getOAuthApp"];
|
||||
/** Update an oauth app */
|
||||
put: operations["OAuthAppsController_updateOAuthApp"];
|
||||
/** Remove a published oauth app */
|
||||
@@ -1609,7 +1640,8 @@ export interface components {
|
||||
SMTP_HOST: string;
|
||||
SMTP_PORT: string;
|
||||
SMTP_USER: string;
|
||||
SMTP_PASS: string;
|
||||
SMTP_PASS?: string | null;
|
||||
SMTP_PASS_ENCRYPTED?: string | null;
|
||||
SMTP_MAX_FREQUENCY: number;
|
||||
MAILER_AUTOCONFIRM: boolean;
|
||||
MAILER_URLPATHS_INVITE: string;
|
||||
@@ -1684,7 +1716,6 @@ export interface components {
|
||||
EXTERNAL_SLACK_SECRET: string;
|
||||
SMS_MESSAGEBIRD_ACCESS_KEY: string;
|
||||
SMS_MESSAGEBIRD_ORIGINATOR: string;
|
||||
SMTP_PASS_ENCRYPTED: string;
|
||||
REFRESH_TOKEN_ROTATION_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_CLIENT_ID: string;
|
||||
@@ -1749,7 +1780,8 @@ export interface components {
|
||||
SMTP_HOST: string;
|
||||
SMTP_PORT: string;
|
||||
SMTP_USER: string;
|
||||
SMTP_PASS: string;
|
||||
SMTP_PASS?: string | null;
|
||||
SMTP_PASS_ENCRYPTED?: string | null;
|
||||
SMTP_MAX_FREQUENCY: number;
|
||||
MAILER_AUTOCONFIRM: boolean;
|
||||
MAILER_URLPATHS_INVITE: string;
|
||||
@@ -1824,7 +1856,6 @@ export interface components {
|
||||
EXTERNAL_SLACK_SECRET: string;
|
||||
SMS_MESSAGEBIRD_ACCESS_KEY: string;
|
||||
SMS_MESSAGEBIRD_ORIGINATOR: string;
|
||||
SMTP_PASS_ENCRYPTED: string;
|
||||
REFRESH_TOKEN_ROTATION_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_CLIENT_ID: string;
|
||||
@@ -1889,7 +1920,8 @@ export interface components {
|
||||
SMTP_HOST: string;
|
||||
SMTP_PORT: string;
|
||||
SMTP_USER: string;
|
||||
SMTP_PASS: string;
|
||||
SMTP_PASS?: string | null;
|
||||
SMTP_PASS_ENCRYPTED?: string | null;
|
||||
SMTP_MAX_FREQUENCY: number;
|
||||
MAILER_AUTOCONFIRM: boolean;
|
||||
MAILER_URLPATHS_INVITE: string;
|
||||
@@ -1964,7 +1996,6 @@ export interface components {
|
||||
EXTERNAL_SLACK_SECRET: string;
|
||||
SMS_MESSAGEBIRD_ACCESS_KEY: string;
|
||||
SMS_MESSAGEBIRD_ORIGINATOR: string;
|
||||
SMTP_PASS_ENCRYPTED: string;
|
||||
REFRESH_TOKEN_ROTATION_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_CLIENT_ID: string;
|
||||
@@ -2995,6 +3026,7 @@ export interface components {
|
||||
status: string;
|
||||
subscription_id: string;
|
||||
is_readonly_mode_enabled?: boolean;
|
||||
preview_branch_refs: (string)[];
|
||||
};
|
||||
AmiSearchOptions: {
|
||||
search_tags?: Record<string, never>;
|
||||
@@ -3034,10 +3066,34 @@ export interface components {
|
||||
status: string;
|
||||
subscription_id: string;
|
||||
is_readonly_mode_enabled?: boolean;
|
||||
preview_branch_refs: (string)[];
|
||||
endpoint: string;
|
||||
anon_key: string;
|
||||
service_key: string;
|
||||
};
|
||||
GetUserContentObject: {
|
||||
owner: {
|
||||
id?: number;
|
||||
username?: string;
|
||||
};
|
||||
updated_by: {
|
||||
id?: number;
|
||||
username?: string;
|
||||
};
|
||||
id: string;
|
||||
inserted_at: string;
|
||||
updated_at: string;
|
||||
type: Record<string, never>;
|
||||
visibility: Record<string, never>;
|
||||
name: string;
|
||||
description?: string;
|
||||
project_id: number;
|
||||
owner_id: number;
|
||||
last_updated_by: number;
|
||||
};
|
||||
GetUserContentResponse: {
|
||||
data: (components["schemas"]["GetUserContentObject"])[];
|
||||
};
|
||||
CreateContentParams: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -3049,6 +3105,30 @@ export interface components {
|
||||
content?: Record<string, never>;
|
||||
owner_id?: number;
|
||||
};
|
||||
UserContentObject: {
|
||||
id: string;
|
||||
inserted_at: string;
|
||||
updated_at: string;
|
||||
type: Record<string, never>;
|
||||
visibility: Record<string, never>;
|
||||
name: string;
|
||||
description?: string;
|
||||
project_id: number;
|
||||
owner_id: number;
|
||||
last_updated_by: number;
|
||||
};
|
||||
UpsertContentParams: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
/** @enum {string} */
|
||||
type: "sql" | "report" | "log_sql";
|
||||
/** @enum {string} */
|
||||
visibility: "user" | "project" | "org" | "public";
|
||||
content?: Record<string, never>;
|
||||
owner_id?: number;
|
||||
project_id: number;
|
||||
};
|
||||
UpdateContentParams: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
@@ -3076,6 +3156,7 @@ export interface components {
|
||||
id: string;
|
||||
name: string;
|
||||
project_ref: string;
|
||||
parent_project_ref: string;
|
||||
is_default: boolean;
|
||||
git_branch: string;
|
||||
created_at: string;
|
||||
@@ -3102,6 +3183,7 @@ export interface components {
|
||||
maxDatabasePreprovisionGb?: number;
|
||||
lastDatabaseResizeAt?: string;
|
||||
preview_branches: (components["schemas"]["BranchResponse"])[];
|
||||
parent_project_ref?: string;
|
||||
};
|
||||
ProjectRefResponse: {
|
||||
id: number;
|
||||
@@ -3126,7 +3208,7 @@ export interface components {
|
||||
back_ups: (components["schemas"]["BackupId"])[];
|
||||
};
|
||||
RestartServiceRequest: {
|
||||
services: ("adminapi" | "api-gateway" | "functions" | "gotrue" | "kong" | "pgbouncer" | "pgsodium" | "postgresql" | "postgrest" | "realtime" | "storage" | "walg")[];
|
||||
services: ("adminapi" | "api-gateway" | "functions" | "gotrue" | "kong" | "pgbouncer" | "pgsodium" | "postgresql" | "postgrest" | "realtime" | "storage" | "walg" | "autoshutdown")[];
|
||||
source_notification_id?: string;
|
||||
region: string;
|
||||
};
|
||||
@@ -3191,6 +3273,11 @@ export interface components {
|
||||
name: string;
|
||||
limit: number;
|
||||
};
|
||||
PreviewTransferInvoiceItem: {
|
||||
description: string;
|
||||
quantity: number;
|
||||
amount: number;
|
||||
};
|
||||
PreviewProjectTransferResponse: {
|
||||
valid: boolean;
|
||||
warnings: (components["schemas"]["PreviewTransferInfo"])[];
|
||||
@@ -3206,6 +3293,8 @@ export interface components {
|
||||
charge_on_target_organization: number;
|
||||
source_subscription_plan: Record<string, never>;
|
||||
target_subscription_plan: Record<string, unknown> | null;
|
||||
source_invoice_items: (components["schemas"]["PreviewTransferInvoiceItem"])[];
|
||||
target_invoice_items: (components["schemas"]["PreviewTransferInvoiceItem"])[];
|
||||
};
|
||||
AnalyticsResponse: {
|
||||
error?: OneOf<[{
|
||||
@@ -3238,6 +3327,7 @@ export interface components {
|
||||
/** @enum {string} */
|
||||
pool_mode: "transaction" | "session" | "statement";
|
||||
max_client_conn?: number | null;
|
||||
connectionString: string;
|
||||
};
|
||||
UpdatePgbouncerConfigBody: {
|
||||
default_pool_size?: number;
|
||||
@@ -3792,6 +3882,34 @@ export interface components {
|
||||
DeleteVercelConnectionResponse: {
|
||||
id: string;
|
||||
};
|
||||
CreateGitHubIntegrationBody: {
|
||||
installation_id: number;
|
||||
organization_slug: string;
|
||||
metadata: Record<string, never>;
|
||||
};
|
||||
CreateGitHubIntegrationResponse: {
|
||||
id: string;
|
||||
};
|
||||
GetGitHubConnections: {
|
||||
id: string;
|
||||
inserted_at: string;
|
||||
updated_at: string;
|
||||
organization_integration_id: string;
|
||||
supabase_project_ref: string;
|
||||
foreign_project_id: string;
|
||||
metadata: Record<string, never>;
|
||||
};
|
||||
CreateGitHubConnectionsBody: {
|
||||
organization_integration_id: string;
|
||||
connection: components["schemas"]["IntegrationConnection"];
|
||||
};
|
||||
GetGithubRepo: {
|
||||
id: number;
|
||||
full_name: string;
|
||||
};
|
||||
GetGithubBranch: {
|
||||
name: string;
|
||||
};
|
||||
FunctionResponse: {
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -4087,19 +4205,56 @@ export interface components {
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
OAuthAppResponse: {
|
||||
id: string;
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
authorized_at?: string;
|
||||
created_at?: string;
|
||||
client_id?: string;
|
||||
client_secret_alias?: string;
|
||||
redirect_uris?: (string)[];
|
||||
};
|
||||
CreateOAuthAppBody: {
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
redirect_uris: (string)[];
|
||||
};
|
||||
UpdateOAuthAppBody: {
|
||||
CreateOAuthAppResponse: {
|
||||
id: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
};
|
||||
PutOAuthAppResponse: {
|
||||
id: string;
|
||||
client_id: string;
|
||||
client_secret_alias: string;
|
||||
created_at: string;
|
||||
name: string;
|
||||
website: string;
|
||||
redirect_uris: (string)[];
|
||||
icon?: string;
|
||||
redirect_uris: (string)[];
|
||||
};
|
||||
TokenDTO: {
|
||||
RevokeAuthorizedOAuthAppResponse: {
|
||||
id: string;
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
authorized_at: string;
|
||||
};
|
||||
DeleteOAuthAppResponse: {
|
||||
id: string;
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
created_at: string;
|
||||
client_id: string;
|
||||
client_secret_alias: string;
|
||||
redirect_uris: (string)[];
|
||||
};
|
||||
OAuthTokenBody: {
|
||||
/** @enum {string} */
|
||||
grant_type: "authorization_code" | "refresh_token";
|
||||
client_id: string;
|
||||
@@ -4109,9 +4264,31 @@ export interface components {
|
||||
redirect_uri?: string;
|
||||
refresh_token?: string;
|
||||
};
|
||||
OAuthTokenResponse: {
|
||||
/** @enum {string} */
|
||||
token_type: "Bearer";
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
};
|
||||
GetAuthorizationResponse: {
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
domain: string;
|
||||
expires_at: string;
|
||||
approved_at?: string;
|
||||
approved_organization_slug?: string;
|
||||
};
|
||||
AuthorizationsApproveBody: {
|
||||
organization_id: string;
|
||||
};
|
||||
ApproveAuthorizationResponse: {
|
||||
url: string;
|
||||
};
|
||||
DeclineAuthorizationResponse: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@@ -6738,11 +6915,38 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["GetUserContentResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Failed to retrieve project's content */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Updates project's content */
|
||||
ContentController_updateWholeContent: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpsertContentParams"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UserContentObject"];
|
||||
};
|
||||
};
|
||||
/** @description Failed to update project's content */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Creates project's content */
|
||||
ContentController_createContent: {
|
||||
parameters: {
|
||||
@@ -6759,7 +6963,7 @@ export interface operations {
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
"application/json": (Record<string, never>)[];
|
||||
"application/json": (components["schemas"]["UserContentObject"])[];
|
||||
};
|
||||
};
|
||||
/** @description Failed to create project's content */
|
||||
@@ -6776,7 +6980,7 @@ export interface operations {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": Record<string, never>;
|
||||
"application/json": components["schemas"]["UserContentObject"];
|
||||
};
|
||||
};
|
||||
/** @description Failed to delete project's content */
|
||||
@@ -6798,7 +7002,7 @@ export interface operations {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (Record<string, never>)[];
|
||||
"application/json": (components["schemas"]["UserContentObject"])[];
|
||||
};
|
||||
};
|
||||
/** @description Failed to update project's content */
|
||||
@@ -8270,6 +8474,102 @@ export interface operations {
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Create github integration */
|
||||
GitHubIntegrationController_createGitHubIntegration: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateGitHubIntegrationBody"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateGitHubIntegrationResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Failed to create github integration */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Gets installed github project connections for the given organization integration */
|
||||
GitHubConnectionsController_getGitHubConnections: {
|
||||
parameters: {
|
||||
path: {
|
||||
organization_integration_id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (components["schemas"]["GetGitHubConnections"])[];
|
||||
};
|
||||
};
|
||||
/** @description Failed to get installed github connections for the given organization integration */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Connects a GitHub project to a supabase project */
|
||||
GitHubConnectionsController_createGitHubConnection: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateGitHubConnectionsBody"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
/** @description Failed to create project connections */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Deletes github project connection */
|
||||
GitHubConnectionsController_deleteGitHubConnection: {
|
||||
parameters: {
|
||||
path: {
|
||||
connection_id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
/** @description Failed to delete github integration project connection */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Gets github repos for the given organization */
|
||||
GitHubRepoController_getRepos: {
|
||||
parameters: {
|
||||
path: {
|
||||
organization_integration_id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (components["schemas"]["GetGithubRepo"])[];
|
||||
};
|
||||
};
|
||||
/** @description Failed to get github repos for the given organization */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Gets github branches for a given repo */
|
||||
GitHubBranchController_getBranches: {
|
||||
parameters: {
|
||||
path: {
|
||||
organization_integration_id: string;
|
||||
repo_owner: string;
|
||||
repo_name: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (components["schemas"]["GetGithubBranch"])[];
|
||||
};
|
||||
};
|
||||
/** @description Failed to get github branches for a given repo */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Gets GoTrue template */
|
||||
AuthTemplateController_getTemplate: {
|
||||
parameters: {
|
||||
@@ -8631,6 +8931,25 @@ export interface operations {
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Processes GitHub event */
|
||||
GitHubWebhooksController_processEvent: {
|
||||
parameters: {
|
||||
header: {
|
||||
"x-github-delivery": string;
|
||||
"x-hub-signature-256": string;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Buffer"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
/** @description Failed to process GitHub event */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Processes Stripe event */
|
||||
StripeWebhooksController_processEvent: {
|
||||
parameters: {
|
||||
@@ -9668,7 +9987,7 @@ export interface operations {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": Record<string, never>;
|
||||
"application/json": (components["schemas"]["OAuthAppResponse"])[];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -9686,12 +10005,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
};
|
||||
};
|
||||
OAuthAppsController_getOAuthApp: {
|
||||
responses: {
|
||||
200: never;
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateOAuthAppResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Update an oauth app */
|
||||
@@ -9704,11 +10022,15 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdateOAuthAppBody"];
|
||||
"application/json": components["schemas"]["CreateOAuthAppBody"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["PutOAuthAppResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Remove a published oauth app */
|
||||
@@ -9720,7 +10042,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeleteOAuthAppResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Revoke an authorized oauth app */
|
||||
@@ -9732,7 +10058,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["RevokeAuthorizedOAuthAppResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Authorize user through oauth */
|
||||
@@ -9750,18 +10080,22 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
303: never;
|
||||
};
|
||||
};
|
||||
/** Exchange auth code for user's access and refresh token */
|
||||
OAuthController_token: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/x-www-form-urlencoded": components["schemas"]["TokenDTO"];
|
||||
"application/x-www-form-urlencoded": components["schemas"]["OAuthTokenBody"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["OAuthTokenResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
AuthorizationsController_getAuthorizationRequest: {
|
||||
@@ -9771,7 +10105,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["GetAuthorizationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Approve oauth app authorization request */
|
||||
@@ -9787,7 +10125,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApproveAuthorizationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Decline oauth app authorization request */
|
||||
@@ -9798,7 +10140,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeclineAuthorizationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { post } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { branchKeys } from './keys'
|
||||
|
||||
export type BranchCreateVariables = {
|
||||
projectRef: string
|
||||
branchName: string
|
||||
gitBranch?: string
|
||||
region?: string
|
||||
}
|
||||
|
||||
export async function createBranch({
|
||||
projectRef,
|
||||
branchName,
|
||||
gitBranch,
|
||||
region,
|
||||
}: BranchCreateVariables) {
|
||||
const { data, error } = await post('/v1/projects/{ref}/branches', {
|
||||
params: {
|
||||
path: { ref: projectRef },
|
||||
},
|
||||
body: {
|
||||
branch_name: branchName,
|
||||
git_branch: gitBranch,
|
||||
region: region,
|
||||
},
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
type BranchCreateData = Awaited<ReturnType<typeof createBranch>>
|
||||
|
||||
export const useBranchCreateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<BranchCreateData, ResponseError, BranchCreateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<BranchCreateData, ResponseError, BranchCreateVariables>(
|
||||
(vars) => createBranch(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(branchKeys.list(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to create branch: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { del } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { branchKeys } from './keys'
|
||||
|
||||
export type BranchDeleteVariables = {
|
||||
id: string
|
||||
projectRef: string
|
||||
}
|
||||
|
||||
export async function deleteBranch({ id }: BranchDeleteVariables) {
|
||||
const { data, error } = await del('/v1/branches/{branch_id}', {
|
||||
params: { path: { branch_id: id } },
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
type BranchDeleteData = Awaited<ReturnType<typeof deleteBranch>>
|
||||
|
||||
export const useBranchDeleteMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<BranchDeleteData, ResponseError, BranchDeleteVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<BranchDeleteData, ResponseError, BranchDeleteVariables>(
|
||||
(vars) => deleteBranch(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(branchKeys.list(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to delete branch: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||