mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
commit
c8f0b14a25
39 files changed
+858
-106
No files matched your search
@@ -1,2 +1,3 @@
|
||||
export const IS_PLATFORM = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
|
||||
export const LOCAL_SUPABASE = process.env.NEXT_PUBLIC_LOCAL_SUPABASE === 'true'
|
||||
export const API_URL = process.env.NEXT_PUBLIC_API_URL
|
||||
@@ -1,7 +1,5 @@
|
||||
interface DataProps {
|
||||
referrer?: string
|
||||
title: string
|
||||
route?: string
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export const post = (url: string, data: DataProps, options = {}) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
import { AuthProvider, ThemeProvider } from 'common'
|
||||
import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
@@ -10,7 +10,7 @@ import { CommandMenuProvider } from 'ui'
|
||||
import components from '~/components'
|
||||
import Favicons from '~/components/Favicons'
|
||||
import SiteLayout from '~/layouts/SiteLayout'
|
||||
import { IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants'
|
||||
import { API_URL, IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants'
|
||||
import { post } from '~/lib/fetchWrappers'
|
||||
import '../styles/ch.scss'
|
||||
import '../styles/main.scss?v=1.0.0'
|
||||
@@ -19,16 +19,21 @@ import '../styles/prism-okaidia.scss'
|
||||
|
||||
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const router = useRouter()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
|
||||
const [supabase] = useState(() =>
|
||||
IS_PLATFORM || LOCAL_SUPABASE ? createBrowserSupabaseClient() : undefined
|
||||
)
|
||||
|
||||
function handlePageTelemetry(route: string) {
|
||||
return post(`https://api.supabase.io/platform/telemetry/page`, {
|
||||
return post(`${API_URL}/telemetry/page`, {
|
||||
referrer: document.referrer,
|
||||
title: document.title,
|
||||
route,
|
||||
ga: {
|
||||
screen_resolution: telemetryProps?.screenResolution,
|
||||
language: telemetryProps?.language,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -45,21 +45,21 @@ These variables will be exposed on the browser, and that's completely fine since
|
||||
[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
|
||||
```ts title=lib/supabase.ts
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import * as SecureStore from 'expo-secure-store'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
|
||||
const ExpoSecureStoreAdapter = {
|
||||
getItem: (key: string) => {
|
||||
return SecureStore.getItemAsync(key);
|
||||
return SecureStore.getItemAsync(key)
|
||||
},
|
||||
setItem: (key: string, value: string) => {
|
||||
SecureStore.setItemAsync(key, value);
|
||||
SecureStore.setItemAsync(key, value)
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
SecureStore.deleteItemAsync(key);
|
||||
SecureStore.deleteItemAsync(key)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
|
||||
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.
|
||||
name: Mendable.ai
|
||||
description: How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.
|
||||
author: rory_wilding
|
||||
description: How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.
|
||||
author: paul_copplestone
|
||||
author_title: Supabase
|
||||
author_url: https://github.com/kiwicopple
|
||||
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
|
||||
@@ -24,31 +24,35 @@ misc: [{ label: 'Backed by', text: 'Y Combinator' }]
|
||||
about: Mendable.ai is Chat Powered Search for Documentation.
|
||||
---
|
||||
|
||||
[Mendable.ai](http://mendable.ai/) provides a chat-powered search engine for technical documentation. Their AI-powered search tool makes it easier for developers and other technical users to find relevant information in complex documentation. Users can simply ask questions in natural language, and the tool returns the most relevant answers. [Mendable.ai](http://mendable.ai/)'s search engine also provides detailed analytics, which helps teams identify knowledge gaps and areas for improvement in their documentation.
|
||||
[Mendable.ai](http://mendable.ai/) provides a chat-powered search engine for technical documentation. Their AI-powered search tool makes it easier for developers and other technical users to find relevant information in complex documentation. Users can simply ask questions in natural language, and the tool returns the most relevant answers. Mendable.ai's search engine also provides detailed analytics, which helps teams identify knowledge gaps and areas for improvement in their documentation.
|
||||
|
||||
## The Challenge
|
||||
|
||||
[Mendable.ai](http://mendable.ai/) needed to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations for their Chat Powered Search for Documentation. They tried Faiss, Weviate, and Pinecone., but found them to be expensive and not very intuitive, especially when it came to storing metadata along with the vectors.
|
||||
Mendable.ai was experiencing tremendous success, with WAUs growing nearly 300% since March, and started looking for a tool to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations for their Chat Powered Search for Documentation. They tried Faiss, Weviate, and Pinecone, but found them to be expensive and not very intuitive, especially when it came to storing metadata along with the vectors.
|
||||
|
||||
Why they chose Supabase:
|
||||
[Mendable.ai](http://mendable.ai/) discovered that Supabase supports pg_vector and found it to be a simple and cost-effective solution. They were impressed with the open-source nature of Supabase, as well as its ability to store metadata alongside the vectors. They also appreciated the intuitive interface and ease of use.
|
||||
Mendable.ai discovered that Supabase supports pg_vector and found it to be a simple and cost-effective solution. They were impressed with the open-source nature of Supabase, as well as its ability to store metadata alongside the vectors. They also appreciated the intuitive interface and ease of use.
|
||||
|
||||
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable.ai">
|
||||
We tried other vector databases - we tried Faiss, we tried Weviate, we tried Pinecone. We found them to be incredibly expensive and not very intuitive. If you’re just doing vector search they’re great, but if you need to store a bunch of metadata that becomes a huge pain.
|
||||
We tried other vector databases - we tried Faiss, we tried Weviate, we tried Pinecone. We found
|
||||
them to be incredibly expensive and not very intuitive. If you’re just doing vector search they’re
|
||||
great, but if you need to store a bunch of metadata that becomes a huge pain.
|
||||
</Quote>
|
||||
|
||||
## What They Built
|
||||
|
||||
Using Supabase's pg_vector, [Mendable.ai](http://mendable.ai/) was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, [Mendable.ai](http://mendable.ai/) was able to quickly and easily search through their customers documentation to find the most relevant responses to queries. They found that Supabase's solution was just as performant as dedicated vector databases, but without the high cost.
|
||||
Using Supabase's pg_vector, Mendable.ai was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, Mendable.ai was able to quickly and easily search through their customers documentation to find the most relevant responses to queries. They found that Supabase's solution was just as performant as dedicated vector databases, but without the high cost.
|
||||
|
||||
## The Results
|
||||
|
||||
Thanks to Supabase's pg_vector, [Mendable.ai](http://mendable.ai/) was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack.
|
||||
Thanks to Supabase's pg_vector, Mendable.ai was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack.
|
||||
|
||||
## Tech stack
|
||||
|
||||
[Mendable.ai](http://mendable.ai/)'s tech stack includes React, Next.js, Express, Vercel, and Supabase.
|
||||
Mendable.ai's tech stack includes React, Next.js, Express, Vercel, and Supabase.
|
||||
|
||||
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable.ai">
|
||||
We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for all the ways we need use it, Supabase has been just as performant - if not more performant - than the other vector databases.
|
||||
We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for
|
||||
all the ways we need use it, Supabase has been just as performant - if not more performant - than
|
||||
the other vector databases.
|
||||
</Quote>
|
||||
@@ -1,12 +1,52 @@
|
||||
import { Button, Badge, IconArrowRight } from 'ui'
|
||||
import SectionHeader from 'components/UI/SectionHeader'
|
||||
import { Badge } from 'ui'
|
||||
import Solutions from 'data/Solutions.json'
|
||||
import Link from 'next/link'
|
||||
import Telemetry from '~/lib/telemetry'
|
||||
import gaEvents from '~/lib/gaEvents'
|
||||
import SectionContainer from '../Layouts/SectionContainer'
|
||||
import ProductIcon from '../ProductIcon'
|
||||
import TextLink from '../TextLink'
|
||||
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const Features = () => {
|
||||
const router = useRouter()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
|
||||
const sendTelemetryEvent = async (product: any) => {
|
||||
switch (product) {
|
||||
case 'Database':
|
||||
return await Telemetry.sendEvent(
|
||||
gaEvents['www_hp_subhero_products_database'],
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
case 'Authentication':
|
||||
return await Telemetry.sendEvent(
|
||||
gaEvents['www_hp_subhero_products_auth'],
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
case 'Storage':
|
||||
return await Telemetry.sendEvent(
|
||||
gaEvents['www_hp_subhero_products_storage'],
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
case 'Edge Functions':
|
||||
return await Telemetry.sendEvent(
|
||||
gaEvents['www_hp_subhero_products_edgeFunctions'],
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
case 'Realtime':
|
||||
return await Telemetry.sendEvent(
|
||||
gaEvents['www_hp_subhero_products_realtime'],
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const IconSections = Object.values(Solutions).map((solution: any) => {
|
||||
const { name, description, icon, label, url } = solution
|
||||
if (solution.name === 'Realtime') return null
|
||||
@@ -24,7 +64,13 @@ const Features = () => {
|
||||
<Badge dot>{label}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{url && <TextLink label={label ? 'Get notified' : 'Learn more'} url={url} />}
|
||||
{url && (
|
||||
<TextLink
|
||||
label={label ? 'Get notified' : 'Learn more'}
|
||||
url={url}
|
||||
onClick={() => sendTelemetryEvent(name)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { Button, IconBookOpen, Space } from 'ui'
|
||||
import Link from 'next/link'
|
||||
import { Button, IconBookOpen } from 'ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import SectionContainer from './Layouts/SectionContainer'
|
||||
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
|
||||
import gaEvents from '~/lib/gaEvents'
|
||||
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
|
||||
|
||||
const Hero = () => {
|
||||
const { basePath } = useRouter()
|
||||
const router = useRouter()
|
||||
const { basePath } = router
|
||||
const telemetryProps = useTelemetryProps()
|
||||
|
||||
const sendTelemetryEvent = async (event: TelemetryEvent) => {
|
||||
await Telemetry.sendEvent(event, telemetryProps, router)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
@@ -40,14 +49,16 @@ const Hero = () => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="https://app.supabase.com" as="https://app.supabase.com" passHref>
|
||||
<a>
|
||||
<a onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_startProject'])}>
|
||||
<Button size="small" className="text-white">
|
||||
Start your project
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="/docs" as="/docs" passHref>
|
||||
<a>
|
||||
<a
|
||||
onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_documentation'])}
|
||||
>
|
||||
<Button size="small" type="default" icon={<IconBookOpen />}>
|
||||
Documentation
|
||||
</Button>
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { IconArrowRight, IconChevronRight } from 'ui'
|
||||
import { IconChevronRight } from 'ui'
|
||||
import Link from 'next/link'
|
||||
|
||||
function TextLink({ url, label }: any) {
|
||||
function TextLink({ url = '', label, ...props }: any) {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
className="text-scale-1100 hover:text-scale-1200 mt-3 block cursor-pointer text-sm"
|
||||
>
|
||||
<div className="group flex items-center gap-1">
|
||||
<span className="sr-only">{`${label} about ${url}`}</span>
|
||||
<span>{label}</span>
|
||||
<div className="transition-all group-hover:ml-0.5">
|
||||
<IconChevronRight size={14} strokeWidth={2} />
|
||||
<Link href={url} passHref>
|
||||
<a
|
||||
className="text-scale-1100 hover:text-scale-1200 mt-3 block cursor-pointer text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className="group flex items-center gap-1">
|
||||
<span className="sr-only">{`${label} about ${url}`}</span>
|
||||
<span>{label}</span>
|
||||
<div className="transition-all group-hover:ml-0.5">
|
||||
<IconChevronRight size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
[
|
||||
{
|
||||
"type": "Customer Story",
|
||||
"title": "Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.",
|
||||
"description": "How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.",
|
||||
"imgUrl": "images/customers/logos/mendableai.png",
|
||||
"logoUrl": "images/customers/logos/mendableai.png",
|
||||
"organization": "Mendable.ai",
|
||||
"url": "/customers/mendableai",
|
||||
"path": "/customers/mendableai",
|
||||
"postMeta": {
|
||||
"name": "Paul Copplestone",
|
||||
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
|
||||
"publishDate": "Feb 16, 2023",
|
||||
"readLength": 6
|
||||
},
|
||||
"ctaText": "View story"
|
||||
},
|
||||
{
|
||||
"type": "Customer Story",
|
||||
"title": "HappyTeams unlocks better performance and reduces cost with Supabase.",
|
||||
|
||||
@@ -23,14 +23,6 @@
|
||||
"label": "",
|
||||
"url": "/storage"
|
||||
},
|
||||
"realtime": {
|
||||
"name": "Realtime",
|
||||
"icon": "M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zM12 2.25V4.5m5.834.166l-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243l-1.59-1.59",
|
||||
"description": "Store, organize, and serve large files. Any media, including videos and images.",
|
||||
"description_short": "",
|
||||
"label": "",
|
||||
"url": "/storage"
|
||||
},
|
||||
"edge-functions": {
|
||||
"name": "Edge Functions",
|
||||
"icon": "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export const APP_NAME = 'Supabase'
|
||||
export const DESCRIPTION = 'The Open Source Alternative to Firebase.'
|
||||
export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'
|
||||
export const IS_PREVIEW = process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
|
||||
export const API_URL = process.env.NEXT_PUBLIC_API_URL
|
||||
|
||||
// Launch Weeek
|
||||
export const SAMPLE_TICKET_NUMBER = 1234
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
interface DataProps {
|
||||
referrer?: string
|
||||
title: string
|
||||
route?: string
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export const post = (url: string, data: DataProps, options = {}) => {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { TelemetryEvent } from './telemetry'
|
||||
|
||||
interface TelemetryEventType {
|
||||
[key: string]: TelemetryEvent
|
||||
}
|
||||
|
||||
const TelemetryEvents: TelemetryEventType = {
|
||||
www_hp_hero_startProject: {
|
||||
action: 'www_hp_hero_startProject',
|
||||
category: 'link',
|
||||
label: '',
|
||||
},
|
||||
www_hp_hero_documentation: {
|
||||
action: 'www_hp_hero_documentation',
|
||||
category: 'link',
|
||||
label: '',
|
||||
},
|
||||
www_hp_subhero_products_database: {
|
||||
action: 'www_hp_subhero_products_database',
|
||||
category: 'link',
|
||||
label: '',
|
||||
},
|
||||
www_hp_subhero_products_auth: {
|
||||
action: 'www_hp_subhero_products_auth',
|
||||
category: 'link',
|
||||
label: '',
|
||||
},
|
||||
www_hp_subhero_products_storage: {
|
||||
action: 'www_hp_subhero_products_storage',
|
||||
category: 'link',
|
||||
label: '',
|
||||
},
|
||||
www_hp_subhero_products_edgeFunctions: {
|
||||
action: 'www_hp_subhero_products_edgeFunctions',
|
||||
category: 'link',
|
||||
label: '',
|
||||
},
|
||||
www_hp_subhero_products_realtime: {
|
||||
action: 'www_hp_subhero_products_realtime',
|
||||
category: 'link',
|
||||
label: '',
|
||||
},
|
||||
}
|
||||
|
||||
export default TelemetryEvents
|
||||
@@ -1913,4 +1913,57 @@ module.exports = [
|
||||
source: '/docs/guides/cli/local-development',
|
||||
destination: '/docs/guides/getting-started/local-development',
|
||||
},
|
||||
|
||||
// old case studies moved to /customers
|
||||
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/case-study-monitoro',
|
||||
destination: '/customers',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/case-study-tayfa',
|
||||
destination: '/customers',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/case-study-xendit',
|
||||
destination: '/customers/xendit',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/case-study-roboflow',
|
||||
destination: '/customers',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/toad-a-link-shortener-with-simple-apis-for-low-coders',
|
||||
destination: '/customers',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/epsilon3-self-hosting',
|
||||
destination: '/customers/epsilon3',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/spot-flutter-with-postgres',
|
||||
destination: '/customers',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/mobbin-supabase-200000-users',
|
||||
destination: '/customers/mobbin',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/replenysh-time-to-value-in-less-than-24-hours',
|
||||
destination: '/customers/replenysh',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/blog/how-supabase-accelerates-development-of-all-pull-together',
|
||||
destination: '/customers',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
import { post } from '~/lib/fetchWrapper'
|
||||
import { API_URL, IS_PROD, IS_PREVIEW } from 'lib/constants'
|
||||
import { NextRouter } from 'next/router'
|
||||
|
||||
export interface TelemetryEvent {
|
||||
category: string
|
||||
action: string
|
||||
label: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
export interface TelemetryProps {
|
||||
screenResolution?: string
|
||||
language: string
|
||||
}
|
||||
|
||||
// This event is the same as in studio/lib/telemetry.tx
|
||||
// but uses different ENV variables for www
|
||||
|
||||
const sendEvent = (event: TelemetryEvent, gaProps: TelemetryProps, router: NextRouter) => {
|
||||
if (!IS_PROD && !IS_PREVIEW) return
|
||||
|
||||
const { category, action, label, value } = event
|
||||
|
||||
return post(`${API_URL}/telemetry/event`, {
|
||||
action: action,
|
||||
category: category,
|
||||
label: label,
|
||||
value: value,
|
||||
page_referrer: document?.referrer,
|
||||
page_title: document?.title,
|
||||
page_location: router.asPath,
|
||||
ga: {
|
||||
screen_resolution: gaProps?.screenResolution,
|
||||
language: gaProps?.language,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
sendEvent,
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
import { APP_NAME, DESCRIPTION } from 'lib/constants'
|
||||
import { API_URL, APP_NAME, DESCRIPTION } from 'lib/constants'
|
||||
import { DefaultSeo } from 'next-seo'
|
||||
import { AppProps } from 'next/app'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import Meta from '~/components/Favicons'
|
||||
import '../styles/index.css'
|
||||
import { post } from './../lib/fetchWrapper'
|
||||
import { AuthProvider, ThemeProvider } from 'common'
|
||||
import { post } from '~/lib/fetchWrapper'
|
||||
import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common'
|
||||
import Head from 'next/head'
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
|
||||
function handlePageTelemetry(route: string) {
|
||||
return post(`https://api.supabase.io/platform/telemetry/page`, {
|
||||
return post(`${API_URL}/telemetry/page`, {
|
||||
referrer: document.referrer,
|
||||
title: document.title,
|
||||
route,
|
||||
ga: {
|
||||
screen_resolution: telemetryProps?.screenResolution,
|
||||
language: telemetryProps?.language,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,42 +5,49 @@
|
||||
<link>https://supabase.com</link>
|
||||
<description>Latest news from Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Wed, 15 Feb 2023 03:03:30 +0000</lastBuildDate>
|
||||
<lastBuildDate>Thu, 04 May 2023 22:10:00 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/customers/mendableai</guid>
|
||||
<title>Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.</title>
|
||||
<link>https://supabase.com/customers/mendableai</link>
|
||||
<description>How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.</description>
|
||||
<pubDate>Thu, 04 May 2023 22:10:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/customers/happyteams</guid>
|
||||
<title>HappyTeams unlocks better performance and reduces cost with Supabase.</title>
|
||||
<link>https://supabase.com/customers/happyteams</link>
|
||||
<description>How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back.</description>
|
||||
<pubDate>Wed, 15 Feb 2023 03:03:30 +0000</pubDate>
|
||||
<pubDate>Wed, 15 Feb 2023 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/customers/xendit</guid>
|
||||
<title>Xendit use Supabase and create a full solution shipped to production in less than one week.</title>
|
||||
<link>https://supabase.com/customers/xendit</link>
|
||||
<description>As a payment processor, Xendit are responsible for verifying that all transactions are legal.</description>
|
||||
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
|
||||
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/customers/replenysh</guid>
|
||||
<title>Replenysh uses Supabase to implement OTP in less than 24 hours.</title>
|
||||
<link>https://supabase.com/customers/replenysh</link>
|
||||
<description>With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres.</description>
|
||||
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
|
||||
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/customers/mobbin</guid>
|
||||
<title>How Mobbin migrated 200,000 users from Firebase for a better authentication experience.</title>
|
||||
<link>https://supabase.com/customers/mobbin</link>
|
||||
<description>Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.</description>
|
||||
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
|
||||
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/customers/epsilon3</guid>
|
||||
<title>Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations.</title>
|
||||
<link>https://supabase.com/customers/epsilon3</link>
|
||||
<description>Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions.</description>
|
||||
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
|
||||
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
|
||||
Generated
+1
-1
@@ -68107,7 +68107,7 @@
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-syntax-highlighter": "*",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk-supabase": "^0.2.2",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './useBreakpoint'
|
||||
export * from './useParams'
|
||||
export * from './useTelemetryProps'
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
export function useTelemetryProps() {
|
||||
const { locale } = useRouter()
|
||||
|
||||
return {
|
||||
screenResolution: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined,
|
||||
language: locale ?? 'en-US',
|
||||
}
|
||||
}
|
||||
@@ -558,6 +558,14 @@
|
||||
"product": "auth",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"id": "exchange-code-for-session",
|
||||
"title": "Exchange an auth code for a session",
|
||||
"slug": "auth-exchangecodeforsession",
|
||||
"product": "auth",
|
||||
"type": "function"
|
||||
|
||||
},
|
||||
{
|
||||
"id": "mfa-enroll",
|
||||
"title": "Enroll a factor",
|
||||
|
||||
+20
-3
@@ -400,6 +400,9 @@ functions:
|
||||
- id: get-session
|
||||
title: 'getSession()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.getSession'
|
||||
notes: |
|
||||
- This method retrieves the current local session (i.e local storage).
|
||||
- If the session has an expired access token, this method will use the refresh token to get a new session.
|
||||
examples:
|
||||
- id: get-the-session-data
|
||||
name: Get the session data
|
||||
@@ -412,8 +415,8 @@ functions:
|
||||
title: 'getUser()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.getUser'
|
||||
notes: |
|
||||
- This method gets the user object from the current session.
|
||||
- Fetches the user object from the database instead of local session.
|
||||
- This method fetches the user object from the database instead of local session.
|
||||
- This method is useful for checking if the user is authorized because it validates the user's access token JWT on the server.
|
||||
- Should be used only when you require the most current user data. For faster results, `getSession().session.user` is recommended.
|
||||
examples:
|
||||
- id: get-the-logged-in-user-with-the-current-existing-session
|
||||
@@ -486,7 +489,7 @@ functions:
|
||||
title: 'refreshSession()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.refreshSession'
|
||||
notes: |
|
||||
- This method will refresh the session whether the current one is expired or not.
|
||||
- This method will refresh and return a new session whether the current one is expired or not.
|
||||
examples:
|
||||
- id: refresh-session-using-the-current-session
|
||||
name: Refresh session using the current session
|
||||
@@ -567,6 +570,20 @@ functions:
|
||||
if (event == 'USER_UPDATED') console.log('USER_UPDATED', session)
|
||||
})
|
||||
```
|
||||
- id: exchange-code-for-session
|
||||
title: 'exchangeCodeForSession()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.exchangeCodeForSession'
|
||||
notes: |
|
||||
- Used when `flowType` is set to `pkce` in client options.
|
||||
examples:
|
||||
- id: exchange-auth-code
|
||||
name: Exchange Auth Code
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
supabase.auth.exchangeCodeForSession('34e770dd-9ff9-416c-87fa-43b31d7ef225')
|
||||
```
|
||||
|
||||
- id: mfa-enroll
|
||||
title: 'mfa.enroll()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueMFAApi.enroll'
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface TaxId {
|
||||
code: string
|
||||
country: string
|
||||
placeholder: string
|
||||
vatPrefix?: string
|
||||
}
|
||||
|
||||
export interface StripeTaxId {
|
||||
@@ -30,6 +31,7 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Austria',
|
||||
placeholder: 'ATU12345678',
|
||||
vatPrefix: 'ATU',
|
||||
},
|
||||
{
|
||||
name: 'AU ABN',
|
||||
@@ -48,12 +50,14 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Belgium',
|
||||
placeholder: 'BE0123456789',
|
||||
vatPrefix: 'BE',
|
||||
},
|
||||
{
|
||||
name: 'BG VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Bulgaria',
|
||||
placeholder: 'BG0123456789',
|
||||
vatPrefix: 'BG',
|
||||
},
|
||||
{
|
||||
name: 'BR CNPJ',
|
||||
@@ -108,6 +112,7 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'ch_vat',
|
||||
country: 'Switzerland',
|
||||
placeholder: 'CHE-123.456.789 MWST',
|
||||
vatPrefix: 'CHE',
|
||||
},
|
||||
{
|
||||
name: 'CL TIN',
|
||||
@@ -120,30 +125,35 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Cyprus',
|
||||
placeholder: 'CY12345678Z',
|
||||
vatPrefix: 'CY',
|
||||
},
|
||||
{
|
||||
name: 'CZ VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Czech Republic',
|
||||
placeholder: 'CZ1234567890',
|
||||
vatPrefix: 'CZ',
|
||||
},
|
||||
{
|
||||
name: 'DE VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Germany',
|
||||
placeholder: 'DE123456789',
|
||||
vatPrefix: 'DE',
|
||||
},
|
||||
{
|
||||
name: 'DK VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Denmark',
|
||||
placeholder: 'DK12345678',
|
||||
vatPrefix: 'DK',
|
||||
},
|
||||
{
|
||||
name: 'EE VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Estonia',
|
||||
placeholder: 'EE123456789',
|
||||
vatPrefix: 'EE',
|
||||
},
|
||||
{
|
||||
name: 'ES CIF',
|
||||
@@ -155,25 +165,29 @@ export const TAX_IDS: TaxId[] = [
|
||||
name: 'ES VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Spain',
|
||||
placeholder: 'ESA1234567Z',
|
||||
placeholder: 'ESX1234567Z',
|
||||
vatPrefix: 'ES',
|
||||
},
|
||||
{
|
||||
name: 'FI VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Finland',
|
||||
placeholder: 'FI12345678',
|
||||
vatPrefix: 'FI',
|
||||
},
|
||||
{
|
||||
name: 'FR VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'France',
|
||||
placeholder: 'FRAB123456789',
|
||||
vatPrefix: 'FR',
|
||||
},
|
||||
{
|
||||
name: 'GB VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'United Kingdom',
|
||||
placeholder: 'GB123456789',
|
||||
vatPrefix: 'GB',
|
||||
},
|
||||
{
|
||||
name: 'GE VAT',
|
||||
@@ -186,6 +200,7 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Greece',
|
||||
placeholder: 'EL123456789',
|
||||
vatPrefix: 'EL',
|
||||
},
|
||||
{
|
||||
name: 'HK BR',
|
||||
@@ -198,12 +213,14 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Croatia',
|
||||
placeholder: 'HR12345678912',
|
||||
vatPrefix: 'HR',
|
||||
},
|
||||
{
|
||||
name: 'HU VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Hungary',
|
||||
placeholder: 'HU12345678912',
|
||||
vatPrefix: 'HU',
|
||||
},
|
||||
{
|
||||
name: 'ID NPWP',
|
||||
@@ -216,6 +233,7 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Ireland',
|
||||
placeholder: 'IE1234567AB',
|
||||
vatPrefix: 'IE',
|
||||
},
|
||||
{
|
||||
name: 'IL VAT',
|
||||
@@ -240,6 +258,7 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Italy',
|
||||
placeholder: 'IT12345678912',
|
||||
vatPrefix: 'IT',
|
||||
},
|
||||
{
|
||||
name: 'JP CN',
|
||||
@@ -270,24 +289,28 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Lithuania',
|
||||
placeholder: 'LT123456789123',
|
||||
vatPrefix: 'LT',
|
||||
},
|
||||
{
|
||||
name: 'LU VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Luxembourg',
|
||||
placeholder: 'LU12345678',
|
||||
vatPrefix: 'LU',
|
||||
},
|
||||
{
|
||||
name: 'LV VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Latvia',
|
||||
placeholder: 'LV12345678912',
|
||||
vatPrefix: 'LV',
|
||||
},
|
||||
{
|
||||
name: 'MT VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Malta',
|
||||
placeholder: 'MT12345678',
|
||||
vatPrefix: 'MT',
|
||||
},
|
||||
{
|
||||
name: 'MX RFC',
|
||||
@@ -318,6 +341,7 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Netherlands',
|
||||
placeholder: 'NL123456789B12',
|
||||
vatPrefix: 'NL',
|
||||
},
|
||||
{
|
||||
name: 'NO VAT',
|
||||
@@ -336,18 +360,21 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Poland',
|
||||
placeholder: 'PL1234567890',
|
||||
vatPrefix: 'PL',
|
||||
},
|
||||
{
|
||||
name: 'PT VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Portugal',
|
||||
placeholder: 'PT123456789',
|
||||
vatPrefix: 'PT',
|
||||
},
|
||||
{
|
||||
name: 'RO VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Romania',
|
||||
placeholder: 'RO1234567891',
|
||||
vatPrefix: 'RO',
|
||||
},
|
||||
{
|
||||
name: 'RU INN',
|
||||
@@ -372,6 +399,7 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Sweden',
|
||||
placeholder: 'SE123456789123',
|
||||
vatPrefix: 'SE',
|
||||
},
|
||||
{
|
||||
name: 'SG GST',
|
||||
@@ -390,12 +418,14 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'Slovenia',
|
||||
placeholder: 'SI12345678',
|
||||
vatPrefix: 'SI',
|
||||
},
|
||||
{
|
||||
name: 'SK VAT',
|
||||
code: 'eu_vat',
|
||||
country: 'Slovakia',
|
||||
placeholder: 'SK1234567891',
|
||||
vatPrefix: 'SK',
|
||||
},
|
||||
{
|
||||
name: 'TH VAT',
|
||||
@@ -426,6 +456,7 @@ export const TAX_IDS: TaxId[] = [
|
||||
code: 'eu_vat',
|
||||
country: 'United Kingdom (Northern Ireland)',
|
||||
placeholder: 'XI123456789',
|
||||
vatPrefix: 'XI',
|
||||
},
|
||||
{
|
||||
name: 'ZA VAT',
|
||||
|
||||
@@ -11,12 +11,11 @@ export const sanitizeTaxID = (taxId: StripeTaxId) => {
|
||||
|
||||
const selectedTaxId = TAX_IDS.find((option) => option.name === taxId.name)
|
||||
|
||||
// The prefix of the country name (ex: "BE" for Belgium, "ESA" for Spain)")
|
||||
const countryPrefix = selectedTaxId?.placeholder.match(/[a-z]+|[^a-z]+/gi)?.[0]
|
||||
const vatIdPrefix = selectedTaxId?.vatPrefix
|
||||
|
||||
// if the value doesn't start with the country code, prepend them
|
||||
if (taxId.type === 'eu_vat' && countryPrefix && !taxId.value.startsWith(countryPrefix)) {
|
||||
newID.value = `${countryPrefix}${taxId.value}`
|
||||
// if the value doesn't start with the prefix, prepend them
|
||||
if (vatIdPrefix && !taxId.value.startsWith(vatIdPrefix)) {
|
||||
newID.value = `${vatIdPrefix}${taxId.value}`
|
||||
}
|
||||
|
||||
return newID
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useProfileQuery } from 'data/profile/profile-query'
|
||||
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.constants'
|
||||
import SQLCard from './SQLCard'
|
||||
import { createSqlSnippetSkeleton } from '../SQLEditor.utils'
|
||||
import { useParams } from 'common'
|
||||
import { useTelemetryProps, useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { SqlSnippet } from 'data/content/sql-snippets-query'
|
||||
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
|
||||
@@ -21,6 +21,7 @@ const SQLTemplates = observer(() => {
|
||||
const { data: profile } = useProfileQuery()
|
||||
const [sql, quickStart] = partition(SQL_TEMPLATES, { type: 'template' })
|
||||
|
||||
const telemetryProps = useTelemetryProps()
|
||||
const snap = useSqlEditorStateSnapshot()
|
||||
const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
resource: { type: 'sql', owner_id: profile?.id },
|
||||
@@ -77,7 +78,8 @@ const SQLTemplates = observer(() => {
|
||||
action: 'script_clicked',
|
||||
label: x.title,
|
||||
},
|
||||
ui.googleAnalyticsProps
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
}}
|
||||
/>
|
||||
@@ -111,7 +113,8 @@ const SQLTemplates = observer(() => {
|
||||
action: 'quickstart_clicked',
|
||||
label: x.title,
|
||||
},
|
||||
ui.googleAnalyticsProps
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,8 @@ import { CSVLink } from 'react-csv'
|
||||
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
|
||||
// @ts-ignore
|
||||
import MarkdownTable from 'markdown-table'
|
||||
import { useTelemetryProps } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export type ResultsDropdownProps = {
|
||||
id: string
|
||||
@@ -17,9 +19,11 @@ export type ResultsDropdownProps = {
|
||||
const ResultsDropdown = ({ id }: ResultsDropdownProps) => {
|
||||
const { project } = useProjectContext()
|
||||
const snap = useSqlEditorStateSnapshot()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
const result = snap.results?.[id]?.[0] ?? undefined
|
||||
const { ui } = useStore()
|
||||
const csvRef = useRef<CSVLink & HTMLAnchorElement & { link: HTMLAnchorElement }>(null)
|
||||
const router = useRouter()
|
||||
|
||||
const csvData = useMemo(
|
||||
() => (result?.rows ? compact(Array.from(result.rows || [])) : ''),
|
||||
@@ -30,7 +34,8 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => {
|
||||
csvRef.current?.link.click()
|
||||
Telemetry.sendEvent(
|
||||
{ category: 'sql_editor', action: 'sql_download_csv', label: '' },
|
||||
ui.googleAnalyticsProps
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,7 +58,8 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => {
|
||||
ui.setNotification({ category: 'success', message: 'Copied results to clipboard' })
|
||||
Telemetry.sendEvent(
|
||||
{ category: 'sql_editor', action: 'sql_copy_as_markdown', label: '' },
|
||||
ui.googleAnalyticsProps
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import { useTheme } from 'common'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import Divider from 'components/ui/Divider'
|
||||
|
||||
export interface APIAuthorizationLayoutProps {}
|
||||
|
||||
const APIAuthorizationLayout = ({ children }: PropsWithChildren<APIAuthorizationLayoutProps>) => {
|
||||
const { isDarkMode } = useTheme()
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Authorize API access | Supabase</title>
|
||||
</Head>
|
||||
<main className="flex-grow flex flex-col w-full h-full overflow-y-auto">
|
||||
<div>
|
||||
<div className="mx-auto px-4 sm:px-6">
|
||||
<div className="max-w-xl flex justify-between items-center mx-auto py-4">
|
||||
<div className="flex justify-start lg:w-0 lg:flex-1">
|
||||
<div>
|
||||
<span className="sr-only">Supabase</span>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? `${BASE_PATH}/img/supabase-dark.svg`
|
||||
: `${BASE_PATH}/img/supabase-light.svg`
|
||||
}
|
||||
alt="Supabase Logo"
|
||||
height={20}
|
||||
width={105}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider light />
|
||||
<div className="flex flex-col justify-center flex-grow mx-auto max-w-[90vw] md:max-w-xl h-full space-y-4">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default APIAuthorizationLayout
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useStore } from 'hooks'
|
||||
import { useTelemetryProps } from 'common'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -25,7 +25,7 @@ function sanitizePageViewRoute(_route?: string) {
|
||||
|
||||
const PageTelemetry: FC = ({ children }) => {
|
||||
const router = useRouter()
|
||||
const { ui } = useStore()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
|
||||
useEffect(() => {
|
||||
function handleRouteChange(url: string) {
|
||||
@@ -71,8 +71,8 @@ const PageTelemetry: FC = ({ children }) => {
|
||||
title: document.title,
|
||||
route,
|
||||
ga: {
|
||||
screen_resolution: ui.googleAnalyticsProps?.screenResolution,
|
||||
language: ui.googleAnalyticsProps?.language,
|
||||
screen_resolution: telemetryProps?.screenResolution,
|
||||
language: telemetryProps?.language,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_ADMIN_URL } from 'lib/constants'
|
||||
|
||||
export type ApiAuthorizationApproveVariables = {
|
||||
id: string
|
||||
organization_id: string
|
||||
}
|
||||
|
||||
export type ApiAuthorizationApproveResponse = {
|
||||
url: string
|
||||
}
|
||||
|
||||
export async function approveApiAuthorization({
|
||||
id,
|
||||
organization_id,
|
||||
}: ApiAuthorizationApproveVariables) {
|
||||
if (!id) throw new Error('Authorization ID is required')
|
||||
if (!organization_id) throw new Error('Organization slug is required')
|
||||
|
||||
const response = await post(
|
||||
`${API_ADMIN_URL}/oauth/authorizations/${id}?skip_browser_redirect=true`,
|
||||
{ organization_id }
|
||||
)
|
||||
if (response.error) throw response.error
|
||||
return response as ApiAuthorizationApproveResponse
|
||||
}
|
||||
|
||||
type ApiAuthorizationApproveData = Awaited<ReturnType<typeof approveApiAuthorization>>
|
||||
|
||||
export const useApiAuthorizationApproveMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<ApiAuthorizationApproveData, unknown, ApiAuthorizationApproveVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
return useMutation<ApiAuthorizationApproveData, unknown, ApiAuthorizationApproveVariables>(
|
||||
(vars) => approveApiAuthorization(vars),
|
||||
options
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import { delete_ } from 'lib/common/fetch'
|
||||
import { API_ADMIN_URL } from 'lib/constants'
|
||||
|
||||
export type ApiAuthorizationDeclineVariables = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type ApiAuthorizationDeclineResponse = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export async function declineApiAuthorization({ id }: ApiAuthorizationDeclineVariables) {
|
||||
if (!id) throw new Error('Authorization ID is required')
|
||||
|
||||
const response = await delete_(`${API_ADMIN_URL}/oauth/authorizations/${id}`, {})
|
||||
if (response.error) throw response.error
|
||||
return response as ApiAuthorizationDeclineResponse
|
||||
}
|
||||
|
||||
type ApiAuthorizationDeclineData = Awaited<ReturnType<typeof declineApiAuthorization>>
|
||||
|
||||
export const useApiAuthorizationDeclineMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<ApiAuthorizationDeclineData, unknown, ApiAuthorizationDeclineVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
return useMutation<ApiAuthorizationDeclineData, unknown, ApiAuthorizationDeclineVariables>(
|
||||
(vars) => declineApiAuthorization(vars),
|
||||
options
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_ADMIN_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { resourceKeys } from './keys'
|
||||
|
||||
export type ApiAuthorizationVariables = {
|
||||
id?: string
|
||||
}
|
||||
|
||||
export type ApiAuthorizationResponse = {
|
||||
name: string
|
||||
website: string
|
||||
domain: string
|
||||
expires_at: string
|
||||
approved_at: string | null
|
||||
approved_organization_slug?: string
|
||||
}
|
||||
|
||||
export async function getApiAuthorizationDetails(
|
||||
{ id }: ApiAuthorizationVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!id) throw new Error('Authorization ID is required')
|
||||
|
||||
const response = await get(`${API_ADMIN_URL}/oauth/authorizations/${id}`, { signal })
|
||||
if (response.error) throw response.error
|
||||
return response as ApiAuthorizationResponse
|
||||
}
|
||||
|
||||
export type ResourceData = Awaited<ReturnType<typeof getApiAuthorizationDetails>>
|
||||
export type ResourceError = { errorEventId: string; message: string }
|
||||
|
||||
export const useApiAuthorizationQuery = <TData = ResourceData>(
|
||||
{ id }: ApiAuthorizationVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<ResourceData, ResourceError, TData> = {}
|
||||
) =>
|
||||
useQuery<ResourceData, ResourceError, TData>(
|
||||
resourceKeys.resource(id),
|
||||
({ signal }) => getApiAuthorizationDetails({ id }, signal),
|
||||
{
|
||||
enabled: enabled && typeof id !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useApiAuthorizationPrefetch = ({ id }: ApiAuthorizationVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (id) {
|
||||
client.prefetchQuery(resourceKeys.resource(id), ({ signal }) =>
|
||||
getApiAuthorizationDetails({ id }, signal)
|
||||
)
|
||||
}
|
||||
}, [id])
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const resourceKeys = {
|
||||
resource: (id: string | undefined) => ['api-authorization', id] as const,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export const organizationKeys = {
|
||||
list: () => ['organizations'] as const,
|
||||
detail: (slug: string | undefined) => ['organizations', slug, 'detail'] as const,
|
||||
roles: (slug: string | undefined) => ['organizations', slug, 'roles'] as const,
|
||||
freeProjectLimitCheck: (slug: string | undefined) =>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { Organization } from 'types'
|
||||
import { organizationKeys } from './keys'
|
||||
|
||||
export type OrganizationsResponse = Organization[]
|
||||
|
||||
export async function getOrganizations(signal?: AbortSignal) {
|
||||
const data = await get(`${API_URL}/organizations`, { signal })
|
||||
if (data.error) throw data.error
|
||||
|
||||
return data as OrganizationsResponse
|
||||
}
|
||||
|
||||
export type OrganizationsData = Awaited<ReturnType<typeof getOrganizations>>
|
||||
export type OrganizationsError = unknown
|
||||
|
||||
export const useOrganizationsQuery = <TData = OrganizationsData>({
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<OrganizationsData, OrganizationsError, TData> = {}) =>
|
||||
useQuery<OrganizationsData, OrganizationsError, TData>(
|
||||
organizationKeys.list(),
|
||||
({ signal }) => getOrganizations(signal),
|
||||
{ enabled: enabled, ...options }
|
||||
)
|
||||
|
||||
export const useOrganizationsPrefetch = () => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(
|
||||
() => client.prefetchQuery(organizationKeys.list(), ({ signal }) => getOrganizations(signal)),
|
||||
[]
|
||||
)
|
||||
}
|
||||
+4
-2
@@ -1,18 +1,20 @@
|
||||
import {
|
||||
AuthContext as AuthContextInternal,
|
||||
AuthProvider as AuthProviderInternal,
|
||||
AuthProviderProps,
|
||||
gotrueClient,
|
||||
useTelemetryProps,
|
||||
} from 'common'
|
||||
import { useProfileQuery } from 'data/profile/profile-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
import { GOTRUE_ERRORS, IS_PLATFORM } from './constants'
|
||||
import Telemetry from 'lib/telemetry'
|
||||
|
||||
export const AuthContext = AuthContextInternal
|
||||
|
||||
export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { ui, app } = useStore()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
|
||||
// Check for unverified GitHub users after a GitHub sign in
|
||||
useEffect(() => {
|
||||
@@ -34,7 +36,7 @@ export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
|
||||
// Track telemetry for the current user
|
||||
useProfileQuery({
|
||||
onSuccess(profile) {
|
||||
ui.setProfile(profile)
|
||||
Telemetry.sendIdentify(profile, telemetryProps)
|
||||
|
||||
// [Joshen] Temp fix: For new users, the GET profile call also creates a default org
|
||||
// But because the dashboard's logged in state is using gotrue as the source of truth
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { User } from 'types'
|
||||
import { NextRouter } from 'next/router'
|
||||
|
||||
export interface GoogleAnalyticsProps {
|
||||
export interface TelemetryProps {
|
||||
screenResolution?: string
|
||||
language: string
|
||||
}
|
||||
@@ -14,7 +15,8 @@ const sendEvent = (
|
||||
label: string
|
||||
value?: string
|
||||
},
|
||||
gaProps?: GoogleAnalyticsProps
|
||||
gaProps: TelemetryProps,
|
||||
router: NextRouter
|
||||
) => {
|
||||
if (!IS_PLATFORM) return
|
||||
|
||||
@@ -25,6 +27,9 @@ const sendEvent = (
|
||||
category: category,
|
||||
label: label,
|
||||
value: value,
|
||||
page_referrer: document?.referrer,
|
||||
page_title: document?.title,
|
||||
page_location: router.asPath,
|
||||
ga: {
|
||||
screen_resolution: gaProps?.screenResolution,
|
||||
language: gaProps?.language,
|
||||
@@ -36,7 +41,7 @@ const sendEvent = (
|
||||
* TODO: GA4 doesn't have identify method.
|
||||
* We may or may not need gaClientId here. Confirm later
|
||||
*/
|
||||
const sendIdentify = (user: User, gaProps?: GoogleAnalyticsProps) => {
|
||||
const sendIdentify = (user: User, gaProps?: TelemetryProps) => {
|
||||
if (!IS_PLATFORM) return
|
||||
|
||||
return post(`${API_URL}/telemetry/identify`, {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createClient } from '@supabase/supabase-js'
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
const supabase = createClient(process.env.SUPABASE_PUBLIC_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useParams } from 'common'
|
||||
import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout'
|
||||
import { FormPanel } from 'components/ui/Forms'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useApiAuthorizationApproveMutation } from 'data/api-authorization/api-authorization-approve-mutation'
|
||||
import { useApiAuthorizationDeclineMutation } from 'data/api-authorization/api-authorization-decline-mutation'
|
||||
import { useApiAuthorizationQuery } from 'data/api-authorization/api-authorization-query'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { Alert, Button, Listbox } from 'ui'
|
||||
|
||||
// Need to handle if no organizations in account
|
||||
// Need to handle if not logged in yet state
|
||||
|
||||
const APIAuthorizationPage: NextPageWithLayout = () => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
const { auth_id } = useParams()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [selectedOrg, setSelectedOrg] = useState<string>()
|
||||
|
||||
const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery()
|
||||
const { data: requester, isLoading, isError, error } = useApiAuthorizationQuery({ id: auth_id })
|
||||
const isApproved = requester?.approved_at !== null
|
||||
const isExpired = dayjs().isAfter(dayjs(requester?.expires_at))
|
||||
|
||||
const { mutateAsync: approveRequest } = useApiAuthorizationApproveMutation()
|
||||
const { mutateAsync: declineRequest } = useApiAuthorizationDeclineMutation()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingOrganizations) {
|
||||
setSelectedOrg(organizations?.[0].slug ?? undefined)
|
||||
}
|
||||
}, [isLoadingOrganizations])
|
||||
|
||||
const onApproveRequest = async () => {
|
||||
if (!auth_id) {
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'Unable to approve request: auth_id is missing ',
|
||||
})
|
||||
}
|
||||
if (!selectedOrg) {
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'Unable to approve request: No organization selected',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true)
|
||||
const res = await approveRequest({ id: auth_id, organization_id: selectedOrg })
|
||||
router.push(res.url)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to approve request: ${error.message}`,
|
||||
})
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onDeclineRequest = async () => {
|
||||
if (!auth_id)
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'Unable to decline request: auth_id is missing ',
|
||||
})
|
||||
|
||||
try {
|
||||
setIsSubmitting(true)
|
||||
await declineRequest({ id: auth_id })
|
||||
ui.setNotification({ category: 'success', message: 'Declined API authorization request' })
|
||||
router.push('/projects')
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to decline request: ${error.message}`,
|
||||
})
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<FormPanel header={<p>Authorize API access</p>}>
|
||||
<div className="w-[500px] px-8 py-6 space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
</FormPanel>
|
||||
)
|
||||
}
|
||||
|
||||
if (auth_id === undefined) {
|
||||
return (
|
||||
<FormPanel header={<p>Authorization for API access</p>}>
|
||||
<div className="w-[500px] px-8 py-6">
|
||||
<Alert withIcon variant="warning" title="Missing authorization ID">
|
||||
Please provide a valid authorization ID in the URL
|
||||
</Alert>
|
||||
</div>
|
||||
</FormPanel>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<FormPanel header={<p>Authorize API access</p>}>
|
||||
<div className="w-[500px] px-8 py-6">
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
title="Failed to fetch details for API authorization request"
|
||||
>
|
||||
<p>Please retry your authorization request from the requesting app</p>
|
||||
{error !== undefined && <p className="mt-2">Error: {error?.message}</p>}
|
||||
</Alert>
|
||||
</div>
|
||||
</FormPanel>
|
||||
)
|
||||
}
|
||||
|
||||
if (isApproved) {
|
||||
const approvedOrganization = organizations?.find(
|
||||
(org) => org.slug === requester.approved_organization_slug
|
||||
)
|
||||
|
||||
return (
|
||||
<FormPanel header={<p>Authorize API access for {requester?.name}</p>}>
|
||||
<div className="w-full md:w-[500px] px-8 py-6 space-y-8">
|
||||
<Alert withIcon variant="success" title="This authorization request has been approved">
|
||||
<p>
|
||||
{requester.name} has read and write access to the organization "
|
||||
{approvedOrganization?.name ?? 'Unknown'}" and all of its projects
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Approved on: {dayjs(requester.approved_at).format('DD MMM YYYY HH:mm:ss (ZZ)')}
|
||||
</p>
|
||||
</Alert>
|
||||
</div>
|
||||
</FormPanel>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormPanel
|
||||
header={<p>Authorize API access for {requester?.name}</p>}
|
||||
footer={
|
||||
<div className="flex items-center justify-end py-4 px-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button type="default" disabled={isSubmitting || isExpired} onClick={onDeclineRequest}>
|
||||
Decline
|
||||
</Button>
|
||||
<Button
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || isExpired}
|
||||
onClick={onApproveRequest}
|
||||
>
|
||||
Authorize {requester?.name}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="w-full md:w-[500px] px-8 py-6 space-y-8">
|
||||
{/* API Authorization requester details */}
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<div>
|
||||
<div className="rounded-md border border-scale-600 p-2.5 flex items-center">
|
||||
<div
|
||||
className="w-8 h-8 md:w-10 md:h-10 bg-center bg-no-repeat bg-cover flex items-center justify-center"
|
||||
// [Joshen] For when we support icons
|
||||
// style={{ backgroundImage: `url('${requester?.icon}')` }}
|
||||
>
|
||||
<p className="text-scale-1000 text-lg">{requester?.name[0]}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-scale-1100">
|
||||
{requester?.name} ({requester?.domain}) is requesting API access to an organization. The
|
||||
application will be able to{' '}
|
||||
<span className="text-amber-1200">
|
||||
read and write the organization's settings and all of its projects.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Expiry warning */}
|
||||
{isExpired && (
|
||||
<Alert withIcon variant="warning" title="This authorization request is expired">
|
||||
Please retry your authorization request from the requesting app
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Organization selection */}
|
||||
{isLoadingOrganizations ? (
|
||||
<div className="py-4 space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
</div>
|
||||
) : (
|
||||
<Listbox
|
||||
label="Select an organization to grant API access to"
|
||||
value={selectedOrg}
|
||||
disabled={isExpired}
|
||||
onChange={setSelectedOrg}
|
||||
>
|
||||
{(organizations ?? []).map((organization) => (
|
||||
<Listbox.Option
|
||||
key={organization.id}
|
||||
label={organization.name}
|
||||
value={organization.slug}
|
||||
>
|
||||
{organization.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
)}
|
||||
</div>
|
||||
</FormPanel>
|
||||
)
|
||||
}
|
||||
|
||||
APIAuthorizationPage.getLayout = (page) => <APIAuthorizationLayout>{page}</APIAuthorizationLayout>
|
||||
export default withAuth(APIAuthorizationPage)
|
||||
@@ -1,8 +1,7 @@
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { action, makeAutoObservable } from 'mobx'
|
||||
import { Project, Notification, User, Organization, ProjectBase, Permission } from 'types'
|
||||
import { Project, Notification, Organization, ProjectBase, Permission } from 'types'
|
||||
import { IRootStore } from './RootStore'
|
||||
import Telemetry, { GoogleAnalyticsProps } from 'lib/telemetry'
|
||||
|
||||
export interface IUiStore {
|
||||
language: 'en-US'
|
||||
@@ -12,12 +11,10 @@ export interface IUiStore {
|
||||
selectedOrganization?: Organization
|
||||
notification?: Notification
|
||||
permissions?: Permission[]
|
||||
googleAnalyticsProps?: GoogleAnalyticsProps
|
||||
load: () => void
|
||||
setProjectRef: (ref?: string) => void
|
||||
setOrganizationSlug: (slug?: string) => void
|
||||
setNotification: (notification: Notification) => string
|
||||
setProfile: (value: User) => void
|
||||
setPermissions: (permissions?: Permission[]) => void
|
||||
}
|
||||
export default class UiStore implements IUiStore {
|
||||
@@ -83,14 +80,6 @@ export default class UiStore implements IUiStore {
|
||||
return undefined
|
||||
}
|
||||
|
||||
get googleAnalyticsProps() {
|
||||
return {
|
||||
screenResolution:
|
||||
typeof window !== 'undefined' ? `${window.innerWidth}x${window.innerHeight}` : undefined,
|
||||
language: this.language,
|
||||
}
|
||||
}
|
||||
|
||||
load() {
|
||||
if (typeof window === 'undefined') return
|
||||
}
|
||||
@@ -109,10 +98,6 @@ export default class UiStore implements IUiStore {
|
||||
return id
|
||||
}
|
||||
|
||||
setProfile(value: User) {
|
||||
Telemetry.sendIdentify(value, this.googleAnalyticsProps)
|
||||
}
|
||||
|
||||
setPermissions(permissions?: any) {
|
||||
this.permissions = permissions
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user