Merge branch 'master' into fix-endpoint

This commit is contained in:
Hieu Pham authored and GitHub committed 2021-12-21 15:26:57 +07:00
commit bf95a7453d
39 files changed
+14745 -399

No files matched your search

+2 -2
View File
@@ -10,9 +10,9 @@ Want to contribute? Why not jump into our GitHub repo and:
- [Sponsor Supabase](https://github.com/sponsors/supabase).
- [Answer Discussions](https://github.com/supabase/supabase/discussions).
- Submit an issue.
- [Submit an issue](https://github.com/supabase/supabase/issue)
- Report a performance issue or a part of the documentation that you find confusing.
- Create a pull request.
- [Create a pull request](https://github.com/supabase/supabase/pulls)
- [Translate our Readme](https://github.com/supabase/supabase/issues/1341).
- Try our products and give feedback.
- Spread the word if you like what we are doing.
+4
View File
@@ -13,4 +13,8 @@ services:
- 5555:8080
db:
volumes:
- /var/lib/postgresql/data
- ./dev/data.sql:/docker-entrypoint-initdb.d/data.sql
storage:
volumes:
- /var/lib/storage
+1 -1
View File
@@ -154,5 +154,5 @@ services:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- /var/lib/postgresql/data
- ./volumes/db/data:/var/lib/postgresql/data
- ./volumes/db/init:/docker-entrypoint-initdb.d
+34 -5
View File
@@ -2,15 +2,32 @@ import Image from 'next/image'
import Link from 'next/link'
import Head from 'next/head'
import { observer } from 'mobx-react-lite'
import { Button, Typography } from '@supabase/ui'
import { Button, IconGitHub, Typography } from '@supabase/ui'
import { API_URL } from 'lib/constants'
import { useStore } from 'hooks'
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
const Landing = () => {
const { ui } = useStore()
const { theme } = ui
async function handleGithubSignIn() {
try {
const { error } = await auth.signIn(
{
provider: 'github',
},
{
redirectTo: process.env.NEXT_PUBLIC_SITE_URL,
}
)
if (error) throw error
} catch (error) {
console.error(error)
}
}
return (
<div className="z-10 relative h-screen flex flex-col">
<Head>
@@ -38,7 +55,13 @@ const Landing = () => {
type="success"
className="ml-8 font-medium hover:text-gray-300 focus:outline-none focus:text-gray-300 transition duration-150 ease-in-out"
>
<Link href={`${API_URL}/login`}>Sign In</Link>
{GOTRUE_ENABLED ? (
<Button onClick={handleGithubSignIn} icon={<IconGitHub />}>
Sign In With Github
</Button>
) : (
<Link href={`${API_URL}/login`}>Sign In</Link>
)}
</Typography.Text>
</div>
</nav>
@@ -55,9 +78,15 @@ const Landing = () => {
</p>
</Typography.Text>
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center space-x-3">
<Link href={`${API_URL}/login`}>
<Button size="large">Sign In</Button>
</Link>
{GOTRUE_ENABLED ? (
<Button onClick={handleGithubSignIn} size="large" icon={<IconGitHub />}>
Sign In With Github
</Button>
) : (
<Link href={`${API_URL}/login`}>
<Button size="large">Sign In</Button>
</Link>
)}
<Link href="https://supabase.com/docs">
<Button size="large" type="default">
Docs
@@ -101,7 +101,7 @@ const LogPanel: FC<Props> = ({
onChange={(e) => onSearch(e.target.value)}
value={searchValue}
actions={
showReset && <IconX size="tiny" className="cursor-pointer mx-1" onClick={onReset} />
showReset && <IconX size="tiny" className="cursor-pointer mx-1" title="Clear search" onClick={onReset} />
}
/>
</div>
@@ -5,6 +5,7 @@ import { observer } from 'mobx-react-lite'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { useStore } from 'hooks'
import WithSidebar from './WithSidebar'
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
/**
* layout for dashboard homepage, account and org settings
@@ -19,6 +20,22 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
const { app, ui } = useStore()
const onClickLogout = async () => {
await auth.signOut()
router.reload()
}
let baseLogoutLink = {
icon: '/icons/feather/power.svg',
label: 'Logout',
href: `${API_URL}/logout`,
key: `${API_URL}/logout`,
}
let logoutLink = GOTRUE_ENABLED
? { ...baseLogoutLink, href: undefined, onClick: onClickLogout }
: baseLogoutLink
const organizationsLinks = app.organizations
.list()
.map((x: any) => ({
@@ -36,7 +53,6 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
isActive: router.pathname == '/',
label: 'All projects',
href: '/',
as: `/`,
},
],
},
@@ -60,12 +76,7 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
href: `/account/me`,
key: `/account/me`,
},
{
icon: '/img/power.svg',
label: 'Logout',
href: `${API_URL}/logout`,
key: `${API_URL}/logout`,
},
logoutLink,
],
},
]
@@ -1,6 +1,7 @@
import Link from 'next/link'
import { CSSProperties, FC, ReactNode } from 'react'
import { Menu, Typography, IconArrowUpRight, Badge } from '@supabase/ui'
import { isUndefined } from 'lodash'
import LayoutHeader from '../ProjectLayout/LayoutHeader'
interface Props {
@@ -142,7 +143,24 @@ const LinksWithoutHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) =>
)
}
const SidebarItem: FC<any> = ({ id, label, as, href, isActive, isSubitem, onClick, external }) => {
const SidebarItem: FC<any> = ({ id, label, href, isActive, isSubitem, onClick, external }) => {
if (isUndefined(href)) {
return (
<Menu.Item
rounded
key={id}
style={{
marginLeft: isSubitem && '.5rem',
}}
active={isActive ? true : false}
onClick={onClick || (() => {})}
icon={external && <IconArrowUpRight size={'tiny'} />}
>
{isSubitem ? <Typography.Text small>{label}</Typography.Text> : label}
</Menu.Item>
)
}
return (
<Link href={href || ''}>
<a className="block" target={external ? '_blank' : '_self'}>
@@ -1,5 +1,8 @@
import { FC } from 'react'
import Link from 'next/link'
import { Typography } from '@supabase/ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Route } from 'components/ui/ui.types'
interface Props {
@@ -9,18 +12,34 @@ interface Props {
const NavigationIconButton: FC<Props> = ({ route, isActive = false }) => {
return (
<Link href={route.link}>
<a
className={[
'flex items-center justify-center h-10 w-10 rounded', // Layout
'text-gray-600 hover:bg-gray-100', // Light mode
'dark:text-gray-400 dark:hover:bg-bg-alt-dark dark:hover:text-white', // Dark mode
`${isActive ? 'bg-gray-100 dark:bg-bg-alt-dark dark:!text-white' : ''}`,
].join(' ')}
>
{route.icon}
</a>
</Link>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Link href={route.link}>
<a
className={[
'flex items-center justify-center h-10 w-10 rounded', // Layout
'text-gray-600 hover:bg-gray-100', // Light mode
'dark:text-gray-400 dark:hover:bg-bg-alt-dark dark:hover:text-white', // Dark mode
`${isActive ? 'bg-gray-100 dark:bg-bg-alt-dark dark:!text-white' : ''}`,
].join(' ')}
>
{route.icon}
</a>
</Link>
</Tooltip.Trigger>
<Tooltip.Content side="right">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'shadow px-2 py-1 rounded border',
'bg-white',
'dark:bg-gray-800 dark:border-gray-700',
].join(' ')}
>
<Typography.Text small>{route.label}</Typography.Text>
</div>
</Tooltip.Content>
</Tooltip.Root>
)
}
@@ -8,6 +8,7 @@ import { API_URL } from 'lib/constants'
import Panel from './Panel'
const { Text, Title } = Typography
const API_SERVICE_ID = 1
export const DisplayApiSettings = () => {
const router = useRouter()
@@ -36,12 +37,9 @@ export const DisplayApiSettings = () => {
</ApiContentWrapper>
)
const { services } = data
// Get the API service
const API_SERVICE_ID = 1
let apiService = services.find((x: any) => x.app.id == API_SERVICE_ID)
let apiKeys = apiService.service_api_keys
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
const apiKeys = apiService.service_api_keys
return (
<ApiContentWrapper>
@@ -124,13 +122,10 @@ export const DisplayConfigSettings = () => {
</ConfigContentWrapper>
)
const { project, services } = data
// Get the API service
const API_SERVICE_ID = 1
let jwtSecret = project.jwt_secret ?? ''
let apiService = services.find((x: any) => x.app.id == API_SERVICE_ID)
let apiConfig = apiService.app_config
const jwtSecret = data?.project.jwt_secret ?? ''
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
const apiConfig = apiService.app_config
return (
<section>
@@ -159,7 +154,7 @@ export const DisplayConfigSettings = () => {
readOnly
copy
reveal
disabled={true}
disabled
value={jwtSecret}
className="input-mono"
descriptionText="Used to decode your JWTs. You can also use this to mint your own JWTs."
+42 -15
View File
@@ -4,6 +4,8 @@ import { readOnly } from './supabaseClient'
import { SupaResponse, User } from 'types'
import { auth0 } from './auth0'
import { flattenNamespaceOnUser } from './apiHelpers'
import { config } from 'process'
import { getAuth0Id, getAuthUser, getIdentity, GOTRUE_ENABLED } from 'lib/gotrue'
/**
* Use this method on api routes to check if user is authenticated and having required permissions.
@@ -32,7 +34,7 @@ export async function apiAuthenticate(
try {
// Check that they are logged in
// If no error throw from getAccessToken. That's good, we can continue
await auth0.getAccessToken(req, res)
if (!(GOTRUE_ENABLED)) await auth0.getAccessToken(req, res)
const user = await fetchUser(req, res)
if (!user) {
@@ -53,16 +55,41 @@ export async function apiAuthenticate(
* user with only id prop or detail object. It depends on requireUserDetail config
*/
async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any> {
const session = auth0.getSession(req, res)
let user_id_supabase = null
let user_id_auth0 = null
let gotrue_id = null
let email = null
if (GOTRUE_ENABLED) {
const token = req.headers.authorization
if (!token) {
return res.status(401).end('Unauthorized: missing access token')
}
let { user: gotrue_user, error: authError } = await getAuthUser(token)
if (authError) {
throw authError
}
if (gotrue_user !== null) {
gotrue_id = gotrue_user?.id
email = gotrue_user.email
if (!session) {
return null
let { identity, error } = getIdentity(gotrue_user)
if (error) throw error
if (identity?.provider !== undefined) {
user_id_auth0 = getAuth0Id(identity?.provider, identity?.id)
}
}
} else {
const session = auth0.getSession(req, res)
if (!session) {
return null
}
const { user: auth0_user } = session
const flattened = flattenNamespaceOnUser('https://supabase.io', auth0_user)
user_id_supabase = flattened.user_id_supabase
user_id_auth0 = flattened.user_id_auth0
email = flattened.email
}
const { user: auth0_user } = session
const flattened = flattenNamespaceOnUser('https://supabase.io', auth0_user)
const { user_id_supabase, user_id_auth0, email } = flattened
if (user_id_supabase) {
return {
id: user_id_supabase,
@@ -70,15 +97,15 @@ async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any
}
}
const { data } = await readOnly
.from('users')
.select(
`
const query = readOnly.from('users').select(
`
id, auth0_id, primary_email, username, first_name, last_name, mobile, is_alpha_user
`
)
.eq('auth0_id', user_id_auth0)
.single()
)
const { data } = GOTRUE_ENABLED
? await query.eq('gotrue_id', gotrue_id).single()
: await await query.eq('auth0_id', user_id_auth0).single()
return data
}
+53
View File
@@ -1,3 +1,6 @@
import { GOTRUE_ENABLED } from 'lib/gotrue'
import { tryParseJson } from 'lib/helpers'
import { isUndefined } from 'lodash'
import { SupaResponse } from 'types/base'
export function handleError<T>(e: any, requestId: string): SupaResponse<T> {
@@ -65,3 +68,53 @@ export async function handleResponseError<T = unknown>(
return { error } as unknown as SupaResponse<T>
}
}
export function getAccessToken() {
// ignore if server-side
if (typeof window === 'undefined') return ''
const tokenData = window?.localStorage['supabase.auth.token']
if (!tokenData) {
// try to get from url fragment
const access_token = getParameterByName('access_token')
if (access_token) return access_token
else return undefined
}
const tokenObj = tryParseJson(tokenData)
if (tokenObj === false) {
return ''
}
return tokenObj.currentSession.access_token
}
// get param from URL fragment
function getParameterByName(name: string, url?: string) {
// ignore if server-side
if (typeof window === 'undefined') return ''
if (!url) url = window?.location?.href || ''
// eslint-disable-next-line no-useless-escape
name = name.replace(/[\[\]]/g, '\\$&')
const regex = new RegExp('[?&#]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url)
if (!results) return null
if (!results[2]) return ''
return decodeURIComponent(results[2].replace(/\+/g, ' '))
}
export function constructHeaders(requestId: string, optionHeaders?: { [prop: string]: any }) {
let headers: { [prop: string]: any } = {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Request-Id': requestId,
...optionHeaders,
}
const hasAuthHeader = !isUndefined(optionHeaders) && 'Authorization' in optionHeaders
if (GOTRUE_ENABLED && !hasAuthHeader) {
const accessToken = getAccessToken()
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
}
return headers
}
+4 -8
View File
@@ -1,4 +1,4 @@
import { handleError, handleResponse, handleResponseError } from './base'
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
import { uuidv4 } from '../../helpers'
import { SupaResponse } from 'types/base'
@@ -8,19 +8,15 @@ export async function delete_<T = any>(
options?: { [prop: string]: any }
): Promise<SupaResponse<T>> {
const requestId = uuidv4()
const { headers, ...otherOptions } = options ?? {}
try {
const { headers: optionHeaders, ...otherOptions } = options ?? {}
const headers = constructHeaders(requestId, optionHeaders)
const response = await fetch(url, {
method: 'DELETE',
body: JSON.stringify(data),
credentials: 'include',
referrerPolicy: 'no-referrer-when-downgrade',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Request-Id': requestId,
...headers,
},
headers,
...otherOptions,
})
+4 -8
View File
@@ -1,4 +1,4 @@
import { handleError, handleResponse, handleResponseError } from './base'
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
import { uuidv4 } from '../../helpers'
import { SupaResponse } from 'types/base'
@@ -8,17 +8,13 @@ export async function get<T = any>(
): Promise<SupaResponse<T>> {
const requestId = uuidv4()
try {
const { headers, ...otherOptions } = options ?? {}
const { headers: optionHeaders, ...otherOptions } = options ?? {}
const headers = constructHeaders(requestId, optionHeaders)
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
referrerPolicy: 'no-referrer-when-downgrade',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Request-Id': requestId,
...headers,
},
headers,
...otherOptions,
})
if (!response.ok) return handleResponseError(response, requestId)
+4 -8
View File
@@ -1,4 +1,4 @@
import { handleError, handleResponse, handleResponseError } from './base'
import { constructHeaders, handleError, handleResponse, handleResponseError } from './base'
import { uuidv4 } from '../../helpers'
import { SupaResponse } from 'types/base'
@@ -9,18 +9,14 @@ export async function patch<T = any>(
): Promise<SupaResponse<T>> {
const requestId = uuidv4()
try {
const { headers, ...otherOptions } = options ?? {}
const { headers: optionHeaders, ...otherOptions } = options ?? {}
const headers = constructHeaders(requestId, optionHeaders)
const response = await fetch(url, {
method: 'PATCH',
body: JSON.stringify(data),
credentials: 'include',
referrerPolicy: 'no-referrer-when-downgrade',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Request-Id': requestId,
...headers,
},
headers,
...otherOptions,
})
+4 -8
View File
@@ -1,4 +1,4 @@
import { handleError, handleResponse, handleResponseError } from './base'
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
import { uuidv4 } from 'lib/helpers'
import { SupaResponse } from 'types/base'
@@ -9,18 +9,14 @@ export async function post<T = any>(
): Promise<SupaResponse<T>> {
const requestId = uuidv4()
try {
const { headers, ...otherOptions } = options ?? {}
const { headers: optionHeaders, ...otherOptions } = options ?? {}
const headers = constructHeaders(requestId, optionHeaders)
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'include',
referrerPolicy: 'no-referrer-when-downgrade',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Request-Id': requestId,
...headers,
},
headers,
...otherOptions,
})
if (!response.ok) return handleResponseError(response, requestId)
+36
View File
@@ -0,0 +1,36 @@
import { GoTrueClient, User } from '@supabase/gotrue-js'
export const GOTRUE_ENABLED = (process.env.NEXT_PUBLIC_GOTRUE_ENABLED === 'true') || false
export const GOTRUE_URL = process.env.NEXT_PUBLIC_GOTRUE_URL || 'http://localhost:8000/auth/v1'
export const auth = new GoTrueClient({
url: GOTRUE_URL,
autoRefreshToken: true,
})
export const getAuthUser = async (token: String): Promise<any> => {
try {
const { data: user, error } = await auth.api.getUser(token.replace('Bearer ', ''))
if (error) throw error
return { user, error: null }
} catch (err) {
console.log(err)
return { user: null, error: err }
}
}
export const getAuth0Id = (provider: String, providerId: String): String => {
return `${provider}|${providerId}`
}
export const getIdentity = (gotrueUser: User) => {
try {
if (gotrueUser !== undefined && gotrueUser.identities !== undefined) {
return { identity: gotrueUser.identities[0], error: null }
}
throw 'Missing identity'
} catch (err) {
return { identity: null, error: err }
}
}
+2 -2
View File
@@ -45,8 +45,8 @@ export const timeout = (ms: number) => {
export const getURL = () => {
const url =
process?.env?.SITE_URL && process.env.SITE_URL !== ''
? process.env.SITE_URL
process?.env?.NEXT_PUBLIC_SITE_URL && process.env.NEXT_PUBLIC_SITE_URL !== ''
? process.env.NEXT_PUBLIC_SITE_URL
: process?.env?.VERCEL_URL && process.env.VERCEL_URL !== ''
? process.env.VERCEL_URL
: 'https://app.supabase.io'
@@ -104,7 +104,16 @@ class StorageExplorerStore {
initializeSupabaseClient = (serviceKey, serviceEndpoint) => {
this.supabaseClient = createClient(
this.normalizeSupabaseURL(`https://${serviceEndpoint}`),
serviceKey
serviceKey,
{
localStorage: {
getItem: (key) => {
return undefined
},
setItem: (key, value) => {},
removeItem: (key) => {},
},
}
)
}
+14182 -183
View File
File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "studio",
"version": "0.0.3",
"version": "0.0.8",
"private": true,
"scripts": {
"dev": "next dev -p 8082",
@@ -15,12 +15,13 @@
"@monaco-editor/react": "^4.3.1",
"@next/bundle-analyzer": "^12.0.4",
"@radix-ui/react-portal": "^0.1.1",
"@radix-ui/react-tooltip": "^0.1.6",
"@sentry/browser": "^6.15.0",
"@sentry/nextjs": "^6.15.0",
"@supabase/grid": "^2.3.9",
"@supabase/postgres-meta": "^0.26.1",
"@supabase/react-data-grid": "^7.1.0-beta.7",
"@supabase/supabase-js": "^1.27.0",
"@supabase/supabase-js": "^1.28.0",
"@supabase/ui": "^0.35.0",
"ajv": "^8.6.3",
"analytics-node": "^3.5.0",
+2 -2
View File
@@ -20,7 +20,7 @@ const Error404: NextPage = ({}) => {
}, [])
return (
<div className="w-full h-screen relative flex flex-col items-center justify-center mx-auto relative">
<div className="w-full h-screen relative flex flex-col items-center justify-center mx-auto">
<div className="absolute top-0 w-full max-w-7xl mx-auto pt-6 px-8 sm:px-6 lg:px-8">
<nav className="relative flex items-center justify-between sm:h-10">
<div className="flex items-center flex-grow flex-shrink-0 lg:flex-grow-0">
@@ -39,7 +39,7 @@ const Error404: NextPage = ({}) => {
</div>
<div className="absolute">
<Typography.Title
className={`filter transition opacity-[5%] duration-200 ${
className={`select-none filter transition opacity-[5%] duration-200 ${
show404 ? 'blur-sm' : 'blur-none'
}`}
style={{ fontSize: '28rem' }}
+2 -3
View File
@@ -78,9 +78,8 @@ const DocView: FC<any> = observer(({}) => {
const swaggerUrl = data?.autoApiService?.restUrl
const headers: any = { apikey: API_KEY }
if(IS_PLATFORM) data?.autoApiService?.endpoint = `https://${data?.autoApiService?.endpoint}`
if (API_KEY?.length > 40) headers['authorization'] = `Bearer ${API_KEY}`
if (IS_PLATFORM) data?.autoApiService?.endpoint = `https://${data?.autoApiService?.endpoint}`
if (API_KEY?.length > 40) headers['Authorization'] = `Bearer ${API_KEY}`
const { data: jsonSchema, error: jsonSchemaError } = useSWR(
() => swaggerUrl,
+3 -1
View File
@@ -116,6 +116,7 @@ export default class MetaStore implements IMetaStore {
connectionString?: string
baseUrl: string
queryBaseUrl: string
excludedSchemas = [
'auth',
'extensions',
@@ -132,6 +133,7 @@ export default class MetaStore implements IMetaStore {
const { projectRef, connectionString } = options
this.rootStore = rootStore
this.baseUrl = `/api/pg-meta/${projectRef}`
this.queryBaseUrl = `${API_URL}/pg-meta/${projectRef}`
const headers: any = {}
if (IS_PLATFORM && connectionString) {
@@ -166,7 +168,7 @@ export default class MetaStore implements IMetaStore {
try {
const headers: any = { 'Content-Type': 'application/json' }
if (this.connectionString) headers['x-connection-encrypted'] = this.connectionString
const url = `${this.baseUrl}/query`
const url = `${this.queryBaseUrl}/query`
const response = await post(url, { query: value }, { headers })
if (response.error) throw response.error
+1 -1
View File
@@ -57,7 +57,7 @@ export default class OpenApiStore implements IOpenApiStore {
const restApiUrl = projectConfig.autoApiService?.restUrl
const response = await get<OpenAPIV2.Document>(`${restApiUrl}?apikey=${apiKey}`, {
headers: { ...headers, apiKey: apiKey, authorization: `Bearer ${apiKey}` },
headers: { ...headers, apiKey: apiKey, Authorization: `Bearer ${apiKey}` },
credentials: 'omit',
})
if (response.error) throw response.error
+7
View File
@@ -310,3 +310,10 @@ label:not(.sbui-formlayout__label):not(.sbui-checkbox__label-container__label):n
cursor: row-resize;
}
}
.radix-tooltip-arrow {
polygon {
@apply dark:fill-[#1f1f1f];
fill: #eeeeee;
}
}
+27
View File
@@ -0,0 +1,27 @@
import { screen, getByText } from '@testing-library/react'
interface SelectorOptions {
container?: HTMLElement
}
/**
* Returns the toggle button given a text matcher
*
* Defaults to screen if container option is not provided
*/
export const getToggleByText = (text: string | RegExp, options: SelectorOptions = {}): HTMLElement | null => {
const container = options?.container
let textNode;
if (container) {
textNode = getByText(container as HTMLElement, text)
}
else {
textNode = screen.getByText(text)
}
if (textNode && textNode.parentElement) {
return textNode.parentElement.querySelector("button[class*='toggle']")
} else {
return textNode
}
}
+5 -5
View File
@@ -1,4 +1,4 @@
import LogPanel from 'components/ui/Logs/LogPanel'
import LogPanel from 'components/interfaces/Settings/Logs/LogPanel'
import { render, waitFor, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@@ -7,7 +7,7 @@ test('templates', async () => {
render(<LogPanel templates={[
{ label: 'Some option', onClick: mockFn }
]} />)
const search = screen.getByPlaceholderText(/Filter/)
const search = screen.getByPlaceholderText(/Search/)
userEvent.type(search, "12345")
@@ -24,7 +24,7 @@ test('templates', async () => {
test("filter input change", async () => {
const mockFn = jest.fn()
render(<LogPanel onSearch={mockFn} />)
const search = screen.getByPlaceholderText(/Filter/)
const search = screen.getByPlaceholderText(/Search/)
userEvent.type(search, "12345")
expect(mockFn).toBeCalled()
})
@@ -49,9 +49,9 @@ test("Manual refresh", async () => {
test("reset filters", () => {
const mockFn = jest.fn()
const { rerender } = render(<LogPanel showReset={false} />)
expect(() => screen.getByText(/Reset filters/)).toThrow()
expect(() => screen.getByText(/Clear search/)).toThrow()
rerender(<LogPanel showReset={true} onReset={mockFn} />)
userEvent.click(screen.getByText(/Reset filters/))
userEvent.click(screen.getByTitle(/Clear search/))
expect(mockFn).toBeCalled()
})
+1 -1
View File
@@ -1,4 +1,4 @@
import LogTable from 'components/ui/Logs/LogTable'
import LogTable from 'components/interfaces/Settings/Logs/LogTable'
import { render, fireEvent, waitFor, screen } from '@testing-library/react'
test('can display log data', async () => {
render(<LogTable data={[{
+90 -76
View File
@@ -4,50 +4,49 @@ import { get } from 'lib/common/fetch'
// mock the settings layout
jest.mock('components/layouts', () => ({
SettingsLayout: jest.fn()
.mockImplementation(({ children }) => <div>{children}</div>)
SettingsLayout: jest.fn().mockImplementation(({ children }) => <div>{children}</div>),
}))
// mock mobx
jest.mock('mobx-react-lite')
import { observer } from 'mobx-react-lite'
observer.mockImplementation(v => v)
observer.mockImplementation((v) => v)
// mock the router
jest.mock('next/router')
import { useRouter } from 'next/router'
useRouter.mockReturnValue({ query: { ref: "123", type: "auth" } })
useRouter.mockReturnValue({ query: { ref: '123', type: 'auth' } })
// mock monaco editor
jest.mock("@monaco-editor/react")
import Editor, { useMonaco } from "@monaco-editor/react"
jest.mock('@monaco-editor/react')
import Editor, { useMonaco } from '@monaco-editor/react'
Editor = jest.fn()
Editor.mockImplementation((props) => {
return (
<textarea
className="monaco-editor"
onChange={e => props.onChange(e.target.value)}
></textarea>
<textarea className="monaco-editor" onChange={(e) => props.onChange(e.target.value)}></textarea>
)
})
useMonaco.mockImplementation(v => v)
useMonaco.mockImplementation((v) => v)
import { LogPage } from 'pages/project/[ref]/settings/logs/[type]'
import { render, fireEvent, waitFor, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {getToggleByText} from "../../helpers"
beforeEach(() => {
get.mockReset()
})
test('can display log data and metadata', async () => {
const data = [{
id: "seome-uuid",
timestamp: 1621323232312,
event_message: "some event happened",
metadata: {
my_key: "something_value"
}
}]
const data = [
{
id: 'seome-uuid',
timestamp: 1621323232312,
event_message: 'some event happened',
metadata: {
my_key: 'something_value',
},
},
]
get.mockResolvedValue({ data })
render(<LogPage />)
@@ -59,15 +58,16 @@ test('can display log data and metadata', async () => {
})
test('Refresh', async () => {
const data = [{
id: "some-uuid",
timestamp: 1621323232312,
event_message: "some event happened",
metadata: {
my_key: "something_value"
}
}]
const data = [
{
id: 'some-uuid',
timestamp: 1621323232312,
event_message: 'some event happened',
metadata: {
my_key: 'something_value',
},
},
]
get.mockResolvedValueOnce({ data }).mockResolvedValueOnce({ data: [] })
render(<LogPage />)
@@ -82,84 +82,98 @@ test('Refresh', async () => {
await waitFor(() => screen.queryByText(/happened/) === null, { timeout: 1000 })
})
test("Search will trigger a log refresh", async () => {
test('Search will trigger a log refresh', async () => {
get.mockImplementation((url) => {
if (url.includes("search_query") && url.includes("something")) {
if (url.includes('search_query') && url.includes('something')) {
return {
data: [{
id: "some-uuid",
timestamp: 1621323232312,
event_message: "some event happened",
metadata: {}
}]
data: [
{
id: 'some-uuid',
timestamp: 1621323232312,
event_message: 'some event happened',
metadata: {},
},
],
}
}
return { data: [] }
})
render(<LogPage />)
userEvent.type(screen.getByPlaceholderText(/Filter/), "something")
await waitFor(() => {
expect(get).toHaveBeenCalledWith(expect.stringContaining("search_query"))
expect(get).toHaveBeenCalledWith(expect.stringContaining("something"))
}, { timeout: 1500 })
userEvent.type(screen.getByPlaceholderText(/Search/), 'something')
await waitFor(
() => {
expect(get).toHaveBeenCalledWith(expect.stringContaining('search_query'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('something'))
},
{ timeout: 1500 }
)
await waitFor(() => screen.getByText(/happened/), { timeout: 1000 })
})
test("poll count for new messages", async () => {
test('poll count for new messages', async () => {
get.mockImplementation((url) => {
if (url.includes("count")) {
return { data: [{ count: 3 }] }
if (url.includes('count')) {
return { data: [{ count: 125 }] }
}
return {
data: [{
id: "some-uuid",
timestamp: 1621323232312,
event_message: "some event happened",
metadata: {}
}]
data: [
{
id: 'some-uuid',
timestamp: 1621323232312,
event_message: 'some event happened',
metadata: {},
},
],
}
})
render(
<LogPage />
)
render(<LogPage />)
await waitFor(() => screen.queryByText(/happened/) === null)
await waitFor(() => screen.getByText(/Load new logs/))
// should display new logs count
await waitFor(() => screen.getByText(/125/))
userEvent.click(screen.getByText(/Load new logs/))
await waitFor(() => screen.queryByText(/Load new logs/) === null)
userEvent.click(screen.getByText(/Refresh/))
await waitFor(() => screen.queryByText(/125/) === null)
await waitFor(() => screen.getByText(/happened/))
})
test("where clause will trigger a log refresh", async () => {
test('where clause will trigger a log refresh', async () => {
get.mockImplementation((url) => {
if (url.includes("where") && url.includes("something")) {
if (url.includes('where') && url.includes('something')) {
return {
data: [{
id: "some-uuid",
timestamp: 1621323232312,
event_message: "some event happened",
metadata: {}
}]
data: [
{
id: 'some-uuid',
timestamp: 1621323232312,
event_message: 'some event happened',
metadata: {},
},
],
}
}
return { data: [] }
})
const { container } = render(<LogPage />)
let editor = container.querySelector('.monaco-editor');
let editor = container.querySelector('.monaco-editor')
expect(editor).toBeFalsy()
userEvent.click(screen.getByText("Custom query"))
editor = container.querySelector('.monaco-editor');
userEvent.type(editor, "metadata.field = something")
// TODO: abstract this out into a toggle selection helper
const toggle = getToggleByText(/via query/)
expect(toggle).toBeTruthy()
userEvent.click(toggle)
await waitFor(() => {
expect(get).toHaveBeenCalledWith(expect.stringContaining("where"))
expect(get).toHaveBeenCalledWith(expect.stringContaining("metadata.field"))
}, { timeout: 1000 })
editor = container.querySelector('.monaco-editor')
expect(editor).toBeTruthy()
})
editor = container.querySelector('.monaco-editor')
userEvent.type(editor, 'metadata.field = something')
await waitFor(
() => {
expect(get).toHaveBeenCalledWith(expect.stringContaining('where'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('metadata.field'))
},
{ timeout: 1000 }
)
await waitFor(() => screen.getByText(/happened/))
})
})
+9 -14
View File
@@ -6,23 +6,21 @@ This is a single repository for two different websites. Our documents were creat
Install the [Vercel CLI](https://vercel.com/cli).
You will need to run the marketing website and docs website separately.
You will need to run the marketing website and docs website separately. To learn how to run both websites, see the [Development Guide](../DEVELOPERS.md) for more information.
```
# tab 1
make dev
```sh
# step 1
cd supabase
# tab 2
# step 2
cd www
# step 3
npm install
npm run dev
# tab 3
cd web/spec
make
npm install
npm run gen:supabase
npm run start
# visit website
http://localhost:3000
```
In production, this setup is deployed as two different vercel websites (`docs` and `www`)
@@ -31,6 +29,3 @@ In production, this setup is deployed as two different vercel websites (`docs` a
- Referencing resources in nested folders may not work for `web` (eg `/web/static/folder/nestedfolder/asset`) may not work.
- Possibly need to prepend all images/assets in docs site with baseUrl.
<!-- // we need to check this -->
<!-- - Don't use `Link` since that adds `/new` to the links. Use plain `a` tags when possible. -->
@@ -0,0 +1,122 @@
---
title: 'Holiday Hackdays Winners 2021'
description: Celebrating many amazing projects submitted to our Holiday Hackdays Hackathon.
author: thor_schaeff
author_url: https://github.com/thorwebdev
author_image_url: https://github.com/thorwebdev.png
authorURL: https://github.com/thorwebdev
image: hackathon-winners/holiday-hackdays/holiday-hackdays-winners-og.png
thumb: hackathon-winners/holiday-hackdays/holiday-hackdays-winners-og.png
tags:
- community
- hackathon
date: '2021-12-17'
toc_depth: 2
---
To cool down from [our latest launch week](/blog/2021/12/15/beta-november-2021-launch-week-recap) we (virtually) [sat down with y'all](https://youtu.be/TijBVcV4jXw) and started hacking, and once again we absolutely loved your hackathon submissions and are excited to send some extremely limited swag your way!
As always, it was challenging to pick the winners from all the great submissions! You can see all the amazing projects submitted on [madewithsupabase.com](https://www.madewithsupabase.com/holiday-hackdays). And now, without further ado, let's look at some of our favourites in more detail:
## Best Overall Project
### Winner
[Swappy.one](https://github.com/zernonia/swappy-one) - by [@zernonia](https://twitter.com/zernonia)
Zernonia is one of our [SupaSquad](https://supabase.com/docs/handbook/supasquad) members and continuously blows our minds with his creativity for the open-source projects he creates. For example, he's also the mastermind behind [madewithsupabase.com](https://www.madewithsupabase.com/), one of our favourite resources to see what y'all are building.
Now he can add a gold tee to his collection, and we're starting to run out of swag ideas to show our appreciation! (Send us some ideas on [Twitter](https://twitter.com/supabase)!)
[![Swappy.one](https://www.madewithsupabase.com/api/resize?link=Swappy.one-rsx33-Frame%2030.png&w=1000)](https://github.com/zernonia/swappy-one)
### Runner Up
[Chivel](https://github.com/lalit2005/chivel) - by [@lalitcodes](https://twitter.com/lalitcodes) and [@ChapagainAshik](https://twitter.com/ChapagainAshik)
Chivel is a tool to quickly generate a landing page for your YouTube channel. For example check out this snazzy page that they've generated for the Supabase channel: [supabase.chivel.tk](https://supabase.chivel.tk/)!
A great idea that deserves some silver tees!
[![Chivel](https://www.madewithsupabase.com/api/resize?link=Chivel-a03sx-ogimage.png&w=1000)](https://github.com/lalit2005/chivel)
## Best Realtime Project
### Winner
[rtPoll](https://github.com/emilioschepis/rtpoll) - by [@emilioschepis](https://twitter.com/emilioschepis) & brother Federico.
Brothers Emilio and Federico built a platform to quickly create polls and share them around. Seeing poll results is great, but you know what's even better? Exactly, seeing poll results in realtime!
[![rtPoll](https://madewithsupabase.com/api/resize?link=RT%20Poll%20-%20Realtime%20Polls-bpl8s-rtpoll-rt.png&w=1000)](https://github.com/emilioschepis/rtpoll)
### Runner Up
[e2ee-chat](https://github.com/arnu515/supabase-e2ee-chat) - by [@arnu5152](https://twitter.com/arnu5152)
A chat app is a fairly obvious use case for realtime syncing, but this project caught our eye because the chat is end-to-end encrypted!
[![e2ee-chat](https://www.madewithsupabase.com/api/resize?link=End-to-end%20encrypted%20chat-eap1qg-Screenshot%20from%202021-12-12%2014-32-08.png&w=1000)](https://github.com/arnu515/supabase-e2ee-chat)
## Best Christmas Themed
### Winner
[Santa Banter](https://github.com/Keoooo/santa-banter) - by [@AndyKeogh](https://twitter.com/AndyKeogh)
Not only do we love a good rhyme, but we also love Christmas themed jokes, so Andy's Santa Banter checks all the boxes. Head over there now to submit your best jokes!
[![Santa Banter](https://madewithsupabase.com/api/resize?link=Santa%20Banter%20-dc8wef-Screenshot%202021-12-07%20at%2009.59.08.png&w=1000)](https://github.com/Keoooo/santa-banter)
### Runner Up
[Holiday Sweater](https://github.com/Morel-Tech/ugly_sweater_app) - by [@mtwichel](https://twitter.com/mtwichel)
Ugly sweaters are an integral part of the Christmas Holidays. With Marcus' awesome Flutter app you can now vote on your favourite sweaters from around the globe!
[![Holiday Sweater](https://madewithsupabase.com/api/resize?link=Holiday%20Sweater%20Contest-pjm3r-Screen%20Shot%202021-12-12%20at%2010.52.24%20PM.png&w=1000)](https://github.com/Morel-Tech/ugly_sweater_app)
## Most Visually Pleasing
### Winner
[the get list](https://github.com/glowdex/wishlist) - by [@glowdexapp](https://github.com/glowdexapp)
This visually stunning website allows you to create a wishlist to share with your friends so they don't need to wreck their brains for gift ideas. Here, have a look at what I want this Christmas: [thegetlist.co/list/thorwebdev](https://www.thegetlist.co/list/thorwebdev).
[![the get list](https://www.madewithsupabase.com/api/resize?link=thegetlist.png&w=1000)](https://github.com/glowdex/wishlist)
### Runner Up
[card creator](https://github.com/maggie-j-liu/card-creator) - by [maggie-j-liu](https://github.com/maggie-j-liu), [Arash](https://github.com/arashnrim), [@eiilla11](https://twitter.com/eiilla11), and [@_pranavnt](https://twitter.com/_pranavnt)
This team of high school hackers just keeps knocking it out of the park again. In our [Hacktoberfest Hackathon](https://supabase.com/blog/2021/10/14/hacktoberfest-hackathon-winners-2021#most-spookyfun) they already made it onto the podium. This time around they built this stunning greeting card platform that lets you send well wishes to your friends around the globe in no time. We love it!
[![card creator](https://madewithsupabase.com/api/resize?link=Card%20Creator-vs9hc-Screen%20Shot%202021-12-12%20at%2011.12.23%20PM.png&w=1000)](https://youtu.be/Yx4N2bONA44)
## The Prizes
As is now tradition with Supabase Hackathons, the winners will receive an extremely limited edition gold medal shirt.
![Gold Tshirt](/images/blog/hackathon-winners/holiday-hackdays/holiday-hackdays-winners-shirt.png)
And the runner uppers will receive the same shirt but in silver. These shirts are limited to winners of the hackathon and will never be produced again. Keep them safe like your favourite NFTs!
## Hang out with us and the community
If you want to join the community and build with us, find other people using Supabase, or if you just want to chill and watch people build, come and join us in Discord!
[Join our Discord](https://discord.supabase.com)
![Discord Hangout](/images/blog/hackathon/community.png)
### Get Started Guides
If you're inspired to build, check out some of the latest resources:
- [Build a SaaS product with Next.js, Supabase and Stripe](https://egghead.io/courses/build-a-saas-product-with-next-js-supabase-and-stripe-61f2bc20)
- [Supabase & Sveltekit - Build Twitter in 75 minutes](https://youtu.be/mPQyckogDYc)
- [Supabase Auth With Next.Js 12 Middleware](https://jitsu.com/blog/supabase-nextjs-middleware#what-were-going-to-build)
- [Vue 3 & Supabase | Workout Tracker App](https://www.youtube.com/watch?v=3tF0fGkd4ho)
- [Use Supabase Auth with Vue.js 3](https://vueschool.io/articles/vuejs-tutorials/use-supabase-auth-with-vue-js-3/)
- [Dynamic Jamstack with Stencil and Supabase](https://ionicframework.com/blog/dynamic-jamstack-with-stencil-and-supabase)
+1 -3
View File
@@ -24,9 +24,7 @@ const BlogListItem = ({ blog }: Props) => {
<Image
layout="fill"
src={
!blog.thumb
? `https://images.unsplash.com/photo-1569982175971-d92b01cf8694?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1`
: `/images/blog/${blog.thumb}`
!blog.thumb ? `/images/blog/blog-placeholder.png` : `/images/blog/${blog.thumb}`
}
objectFit="cover"
className="transform duration-100 ease-in scale-100 group-hover:scale-105"
+4 -2
View File
@@ -24,7 +24,7 @@ const Footer = (props: Props) => {
<SectionContainer>
<div className="xl:grid xl:grid-cols-3 xl:gap-8">
<div className="space-y-8 xl:col-span-1">
<Link href="#" as=" ">
<Link href="#" as="/">
<a>
<img
className="w-40"
@@ -102,7 +102,9 @@ const Footer = (props: Props) => {
</div>
</div>
<div className="mt-32 border-t dark:border-dark pt-8 flex justify-between">
<p className="text-base text-gray-400 dark:text-dark-400">&copy; Supabase Inc</p>
<p className="mb-0 self-center text-base text-gray-400 dark:text-dark-400">
&copy; Supabase Inc
</p>
<DarkModeToggle darkMode={darkMode} updateTheme={updateTheme} />
</div>
</SectionContainer>
+1 -4
View File
@@ -27,10 +27,7 @@ const Hero = () => {
<p className="mt-3 text-base">Serverless functions coming soon</p>
</Typography.Text>
<Space>
<Link
href="https://api.supabase.io/platform/login"
as="https://api.supabase.io/platform/login"
>
<Link href="https://app.supabase.io" as="https://app.supabase.io">
<a className="mt-10">
<Button className="py-3" size="medium">
Start your project
+7 -9
View File
@@ -3,15 +3,13 @@ import Link from 'next/link'
function TextLink({ url, label }: any) {
return (
<a href={url}>
<a className="block text-sm text-gray-400 dark:text-gray-400 mt-3">
<div className="flex items-center">
<span>{label}</span>
<span className="ml-2">
<IconArrowRight size="tiny" />
</span>
</div>
</a>
<a href={url} className="block text-sm text-gray-400 dark:text-gray-400 mt-3">
<div className="flex items-center">
<span>{label}</span>
<span className="ml-2">
<IconArrowRight size="tiny" />
</span>
</div>
</a>
)
}
+2 -2
View File
@@ -50,12 +50,12 @@ const Index = () => {
</div>
</SectionContainer>
<SectionContainer>
<div className="grid grid-cols-12 border dark:border-gray-600">
<div className="grid grid-cols-12 border dark:border-gray-600 rounded-lg shadow-small">
<div className="col-span-12 lg:col-span-5">
<img
src={`/brand-assets/logo-preview.jpg`}
width="100%"
className="object-cover h-full"
className="object-cover h-full rounded-l-lg"
/>
</div>
<div className="col-span-12 lg:col-span-7 flex items-center">
Binary file not shown.

After

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB