Merge pull request #13842 from supabase/master

Prod deploy dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-04-20 13:35:24 +07:00
commit fedf6a14b0
73 files changed
+1005 -4488

No files matched your search

@@ -888,6 +888,7 @@ export const integrations = {
{ name: 'Authsignal', url: '/guides/integrations/authsignal' },
{ name: 'Clerk', url: '/guides/integrations/clerk' },
{ name: 'keyri', url: '/guides/integrations/keyri' },
{ name: 'Passage', url: '/guides/integrations/passage' },
{ name: 'Stytch', url: '/guides/integrations/stytch' },
{ name: 'SuperTokens', url: '/guides/integrations/supertokens' },
],
+1 -1
View File
@@ -94,7 +94,7 @@ export const examples = [
{
name: 'Connect to Postgres',
description: `Connecting to Postgres from Edge Functions.`,
href: '/guides/functions/examples/connect-to-postgres',
href: '/guides/functions/connect-to-postgres',
},
{
name: 'Github Actions',
@@ -162,7 +162,7 @@ Open `pages/_app.js` and wrap our `Component` with the `UserProvider` from Auth0
// pages/_app.js
import React from 'react'
import { UserProvider } from '@auth0/nextjs-auth0'
import { UserProvider } from '@auth0/nextjs-auth0/client'
const App = ({ Component, pageProps }) => {
return (
@@ -0,0 +1,608 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'passage',
title: 'Passage',
description:
'Build a Next.js application that uses passkey authentication with Passage and Row Level Security (RLS) with Supabase.',
}
This guide steps through building a Next.js application with Passage and Supabase. We will use Passage to authenticate users and manage tokens, while using Supabase for storing data and enforcing authorization logic using Row Level Security policies.
The full code example for this guide can be found [here](https://github.com/passageidentity/supabase-passage-example).
[Passage](https://passage.id) is a passwordless authentication platform that makes it simple for developers to add passkey authentication to their apps and websites, providing better security and simpler sign in for your users. They provide simple frontend components that handle all of the complexity of passwordless login for developers in just two lines of codes. Passage also provides session management, user management, and in-depth customization capabilities.
Next.js is a web application framework built on top of React. We will be using it for this example, as it allows us to write server-side logic within our application. Passage’s [frontend elements](https://docs.passage.id/frontend/passage-element) and [Node.js SDK](https://docs.passage.id/backend/overview/node) are designed to work for Next.js.
For this guide, you will need a Passage account which can be created [here](https://console.passage.id/register), and a Supabase account which can be created [here](https://app.supabase.com/).
## 1. Create a Passage application
In the [Passage Console](https://console.passage.id), create a new application with the following configuration:
* Application name: Todo Application
* Authentication origin: http://localhost:3000
* Redirect URL: /dashboard
![Passage Console dashboard](/docs/img/guides/integrations/passage/01.png)
![Fill out the required fields to create a new application](/docs/img/guides/integrations/passage/02.png)
## 2. Create and configure a Supabase project
#### Create a new project
In the [Supabase dashboard](https://app.supabase.com/), click `New Project`.
Enter a name for your project and create a secure database password.
#### Create a table schema
We are building Todo list application, similar to the [Supabase demo application](https://github.com/supabase/supabase/tree/master/examples/todo-list/nextjs-todo-list) so we will need a table for the todo list items.
Create a new table in the Table Editor view.
Set the Name field to `todo`.
Select Enable Row Level Security (RLS).
Create the following new columns.
* `title` as `text`
* `user_id` as `text` with a default value of `auth.user_id()`
* `is_complete` as `bool` with a default value of `false`
Click `Save` to create the table.
![Table schema.](/docs/img/guides/integrations/passage/03.png)
#### Add initial data to the table
From the Table editor, select the `todo` table and click `Insert row`. Fill out the required fields with an example todo item, leaving the `user_id` as NULL and click `Save`.
![Example todo item.](/docs/img/guides/integrations/passage/04.png)
After adding a few todo items, the table editor view will look like this:
![Table with multiple todo items.](/docs/img/guides/integrations/passage/05.png)
## 3. Build a Next.js app
#### Create Next.js app
Create a new Next.js project on the command line. You can choose your settings through the setup wizard - for this guide we will use JavaScript instead of TypeScript. This example uses the create script in Next v13.2.
```sh
npx create-next-app <name-of-project>
cd <name-of-project>/
```
You should be able to use the default settings for the project, but here are the settings used for the example app.
![Settings for Next.js application.](/docs/img/guides/integrations/passage/06.png)
#### Configure your ENV
Create a `.env` file and enter the following values.
```sh
NEXT_PUBLIC_PASSAGE_APP_ID=get-from-passage-settings
PASSAGE_API_KEY=get-from-passage-settings
NEXT_PUBLIC_SUPABASE_URL=get-from-supabase-dashboard
NEXT_PUBLIC_SUPABASE_ANON_KEY=get-from-supabase-dashboard
SUPABASE_JWT_SECRET=get-from-supabase-dashboard
```
The Supabase values can be found under `Project->Settings->API Settings`.
![Supabase environment variables.](/docs/img/guides/integrations/passage/07.png)
The Passage values can be found under `General->Settings` and `General->API Keys`.
![Passage environment variables.](/docs/img/guides/integrations/passage/08.png)
![Passage environment variables.](/docs/img/guides/integrations/passage/09.png)
> The `PASSAGE_API_KEY` and `SUPABASE_JWT_SECRET` are secret values and should never be shared publicly. They will only be used in the server-side code of the Next.js application.
Restart your Next.js development server to read in the environment variables.
```bash
npm run dev
```
## 4. Add Passage login to your app
#### Add Passage Element
Install the `@passageidentity/passage-elements` package.
```bash
npm install @passageidentity/passage-elements
```
Create a new folder called components with a new login file `components/login.js` and add the following content.
```js
// components/login.js
import { useEffect } from 'react';
const PassageLogin = () => {
useEffect(()=>{
require('@passageidentity/passage-elements/passage-auth');
}, []);
return (
<>
<passage-auth app-id={process.env.NEXT_PUBLIC_PASSAGE_APP_ID}></passage-auth>
</>
)
}
export default PassageLogin
```
Then update `pages/index.js` to include the login component.
```js
// pages/index.js
import styles from '@/styles/Home.module.css'
import PassageLogin from '@/components/login'
export default function Home(props) {
return(
<div className={styles.main}>
<PassageLogin />
</div>
)
}
```
When we have a successful registration the Passage element will request a redirect to `/dashboard` per the redirect URL we set during app creation.
Create a new file `pages/dashboard.js` for this new route with the following content:
```js
// pages/dashboard.js
import styles from '@/styles/Home.module.css'
export default function Dashboard({isAuthorized, userID, todos}) {
return(
<div className={styles.main}>
<div className={styles.container}>
<p>
You've logged in!
</p>
</div>
</div>
)
}
```
Now when you visit `http://localhost:3000` in a browser you will have a fully functioning and passwordless login page!
![Simple app with Passage login page.](/docs/img/guides/integrations/passage/10.png)
Go ahead and go through the registration process. You will be able to register an account with either a passkey or a magic link. Once you've logged in, you will notice that you just get redirected to `/dashboard` page.
The login was successful, but we need to build in the functionality to know when a user is authenticated and show them the appropriate view.
#### Use Passage to verify the JWT
Now we will need to use a Passage SDK to verify the JWT from Passage.
Install the Passage Node.js library.
```bash
npm install @passageidentity/passage-node
```
Create a utils folder and a file called `utils/passage.js` with the following content.
```js
// utils/passage.js
import Passage from '@passageidentity/passage-node';
const passage = new Passage({
appID: process.env.NEXT_PUBLIC_PASSAGE_APP_ID,
apiKey: process.env.PASSAGE_API_KEY,
});
export const getAuthenticatedUserFromSession = async (req, res) => {
try {
const userID = await passage.authenticateRequest(req);
if (userID) {
return {isAuthorized: true, userID: userID};
}
} catch (error) {
// authentication failed
return {isAuthorized: false, userID: ''};
}
}
```
This will be used in the `getServerSideProps()` function to check authentication status for a user. Add this function to `index.js` then update the `Home` function to use the props.
```js
// pages/index.js
import styles from '@/styles/Home.module.css'
import PassageLogin from '@/components/login'
import { getAuthenticatedUserFromSession } from '@/utils/passage'
import { useEffect } from 'react'
import Router from 'next/router';
export default function Home({isAuthorized}) {
useEffect(()=> {
if(isAuthorized){
Router.push('/dashboard')
}
})
return(
<div className={styles.main}>
<PassageLogin />
</div>
)
}
export const getServerSideProps = async (context) => {
const loginProps = await getAuthenticatedUserFromSession(context.req, context.res)
return {
props: {
isAuthorized: loginProps.isAuthorized?? false,
userID: loginProps.userID?? ''
},
}
}
```
We will also use this logic on the dashboard page to check if a user is authenticated. If not we should redirect them to the login page. We will also add a quick sign out button using Passage while we are at it.
```js
// pages/dashboard.js
import styles from '@/styles/Home.module.css'
import { useEffect } from 'react';
import Router from 'next/router';
import { getAuthenticatedUserFromSession } from '@/utils/passage'
import { PassageUser } from '@passageidentity/passage-elements/passage-user'
export default function Dashboard({isAuthorized, userID}) {
useEffect(() => {
if(!isAuthorized){
Router.push('/');
}
})
const signOut = async ()=>{
new PassageUser().signOut()
Router.push('/')
}
return (
<div className={styles.main}>
<h1>
Welcome {userID}!{' '}
</h1>
<br></br>
<button onClick={signOut}>Sign Out</button>
</div>
)
}
export const getServerSideProps = async (context) => {
const loginProps = await getAuthenticatedUserFromSession(context.req, context.res)
return {
props: {
isAuthorized: loginProps.isAuthorized?? false,
userID: loginProps.userID?? '',
},
}
}
```
The app can now tell the difference between an authenticated and unauthenticated user. When you log into the application, you will be redirected to the dashboard and see this message.
![Authenticated users can see their user ID.](/docs/img/guides/integrations/passage/11.png)
## 5. Integrate Supabase into Next.js app
Passage and Supabase do not currently allow for custom signing secrets. Therefore, we will need to extract the necessary claims from the Passage JWT and sign a new JWT to send to Supabase.
Because of the sensitive nature of this functionality, we will handle the authentication and JWT exchange in Next.js’s server-side rendering function `getServerSideProps()`. Imports used in this function will not be bundled client-side. Additionally, the JWT provided by Passage is stored in a cookie which is automatically passed to `getServerSideProps()`.
#### Sign Passage token for Supabase
Install the Supabase client SDK and the popular Node package `jsonwebtoken`, which allows us to easily work with JWTs.
```bash
npm install @supabase/supabase-js jsonwebtoken
```
Create a new file called `utils/supabase.js` and add the following content. This function accepts a Passage user ID and then creates and signs a Supabase JWT for that user. This allows Supabase to verify the token and authenticate the user when making Supabase calls.
```js
// utils/supabase.js
import { createClient } from '@supabase/supabase-js'
import jwt from 'jsonwebtoken'
const getSupabase = (userId) => {
const options = {}
if (userId) {
const payload = {
userId,
exp: Math.floor(Date.now() / 1000) + 60 * 60,
}
const token = jwt.sign(payload, process.env.SUPABASE_JWT_SECRET)
options.global = {
headers: {
Authorization: `Bearer ${token}`,
},
}
}
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
options
)
return supabase
}
export { getSupabase }
```
#### Enable Row Level Security (RLS) in Supabase
To enable users to view and create their own todo items we need to write a RLS policy. Our policy will check the currently logged in user is to determine whether or not they should have access. Let's create a PostgreSQL function to extract the current user from our new JWT.
Navigate back to the Supabase dashboard, select `SQL Editor` from the sidebar menu, and click `New query`. This will create a new query called `new sql snippet`, which will allow us to run any SQL against our Postgres database.
Write the following and click `Run`.
```sql
create or replace function auth.user_id() returns text as $$
select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text;
$$ language sql stable;
```
You should see the output `Success, no rows returned`. This created a function called `auth.user_id()` which will return the `userId` field of our JWT payload.
> To learn more about PostgresSQL functions, check out this [deep dive video](https://www.youtube.com/watch?v=MJZCCpCYEqk).
Now we can create a policy that checks whether the current user is the owner of a todo item. From the `Authentication` sidebar menu in Supabase, click `Policies` then create a new policy.
![RLS policies for a table.](/docs/img/guides/integrations/passage/12.png)
Choose `For full customization create a policy from scratch` and add the following.
![Policy to restrict access to todo items.](/docs/img/guides/integrations/passage/13.png)
This policy is calling the function we just created to get the currently logged in user's ID `auth.user_id() `and checking whether this matches the `user_id` column for the current todo. If it does, then it will allow the user to select it, otherwise it will deny access.
Click `Review` and then `Save policy`.
> Note: To learn more about RLS and policies, check out this [deep dive video](https://www.youtube.com/watch?v=Ow_Uzedfohk).
#### Fetch data from Supabase
Now we can fetch data from Supabase specific to that user. We will update `pages/dashboard.js` to do the following:
1. authenticate the user using Passage
2. create and sign a JWT for the user with the Supabase secret
3. query Supabase to fetch a user’s todo list items
```js
// pages/dashboard.js
import styles from '@/styles/Home.module.css'
import { useEffect } from 'react';
import Router from 'next/router';
import { getAuthenticatedUserFromSession } from '@/utils/passage'
import { getSupabase } from '../utils/supabase'
export default function Dashboard({isAuthorized, userID, todos}) {
useEffect(() => {
if(!isAuthorized){
Router.push('/');
}
})
return(
<div className={styles.main}>
<div className={styles.container}>
<h1>
Welcome {userID}!{' '}
</h1>
<br></br>
<button onClick={signOut}>Sign Out</button>
<br></br>
<div className={styles.list}>
{todos?.length > 0 ? (
todos.map((todo) => <li key={todo.id}>{todo.title}</li>)
) : (
<p>You have completed all todos!</p>
)}
</div>
</div>
</div>
)
}
export const getServerSideProps = async (context) => {
const loginProps = await getAuthenticatedUserFromSession(context.req, context.res)
if(loginProps.isAuthorized){
const supabase = getSupabase(loginProps.userID)
const {data} = await supabase.from('todo').select()
return {
props: {
isAuthorized: loginProps.isAuthorized?? false,
userID: loginProps.userID?? '',
todos: data?? [],
},
}
} else {
return {
props: {
isAuthorized: loginProps.isAuthorized?? false,
userID: loginProps.userID?? ''
},
}
}
}
```
When we reload our application, we are still getting the empty state for todos.
This is because we enabled Row Level Security, which blocks all requests by default and lets you granularly control access to the data in your database.
#### Update the UserID data
The last thing we need to do is update the `user_id` columns for our existing todos. Head back to the Supabase dashboard, and select `Table editor` from the sidebar. You will see that the `user_id` field is NULL for all of our todo items.
![User ID is NULL for all todo items.](/docs/img/guides/integrations/passage/14.png)
To get the user ID for our Passage user, go back to the Passage Console and check the `Users` tab.
![Get User ID from Passage.](/docs/img/guides/integrations/passage/15.png)
Copy this user ID and update two of the three rows in the Supabase database to match this user ID. When you are done, the database table will look like this.
![Updated User ID in Supabase.](/docs/img/guides/integrations/passage/16.png)
Now when we refresh the application, we will see the todo items for our user!
![Authenticated users can see their todo items.](/docs/img/guides/integrations/passage/17.png)
## Bonus: Add todo items
To build out a bit more functionality in our application, we can now let users add items to their to do list. Create a file `pages/api/addTodo.js` with the following content.
```js
// pages/api/addTodo.js
import { getSupabase } from "../../utils/supabase";
export default async function handler(req, res) {
const { userID, todo } = req.body;
const supabase = getSupabase(userID);
const { data, error } = await supabase
.from("todo")
.insert({ title: todo })
.select()
.single();
if (error) return res.status(400).json(error);
res.status(200).json(data);
}
```
Then update `pages/dashboard.js` to include a form for submitting new to do items. The complete file will look like this.
```js
//pages/dashboard.js
import styles from "@/styles/Home.module.css";
import { useEffect, useState } from "react";
import Router from "next/router";
import { getAuthenticatedUserFromSession } from "@/utils/passage";
import { getSupabase } from "../utils/supabase";
import { PassageUser } from "@passageidentity/passage-elements/passage-user";
export default function Dashboard({ isAuthorized, userID, initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
useEffect(() => {
if (!isAuthorized) {
Router.push("/");
}
});
const handleSubmit = async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const todo = data.get("todo");
const res = await fetch("/api/addTodo", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ todo, userID }),
}).then((res) => res.json());
setTodos([...todos, res]);
};
const signOut = async () => {
new PassageUser().signOut();
Router.push("/");
};
return (
<div className={styles.main}>
<div className={styles.container}>
<h1>Welcome {userID}! </h1>
<br></br>
<button onClick={signOut}>Sign Out</button>
<br></br>
<div className={styles.list}>
{todos?.length > 0 ? (
todos.map((todo) => <li key={todo.id}>{todo.title}</li>)
) : (
<p>You have completed all todos!</p>
)}
</div>
<form onSubmit={handleSubmit}>
<label>
Todo: <input type="text" name="todo" />
</label>
<button>Submit</button>
</form>
</div>
</div>
);
}
export const getServerSideProps = async (context) => {
const loginProps = await getAuthenticatedUserFromSession(
context.req,
context.res
);
if (loginProps.isAuthorized) {
const supabase = getSupabase(loginProps.userID);
const { data } = await supabase
.from("todo")
.select()
.is("is_complete", false);
return {
props: {
isAuthorized: loginProps.isAuthorized ?? false,
userID: loginProps.userID ?? "",
initialTodos: data ?? [],
},
};
} else {
return {
props: {
isAuthorized: loginProps.isAuthorized ?? false,
userID: loginProps.userID ?? "",
},
};
}
};
```
Finally, we need to add a new RLS policy in Supabase to allow users to insert their own todo items.
![RLS policy for inserting todo items.](/docs/img/guides/integrations/passage/18.png)
That's it! Now the website has form for submitting new items for the to do list.
![Authenticated users can create todo items](/docs/img/guides/integrations/passage/19.png)
## Resources
* [Passage website](https://passage.id)
* [Complete developer documentation](https://docs.passage.id)
* [Passage Github](https://github.com/passageidentity), including SDKs and example apps for Next.js
* [Developer community](https://discord.com/invite/445QpyEDXh) on Discord
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -72,7 +72,7 @@ Click `Save` to create the new table.
Now we want to make sure that only the `todos` owner, the user's `wallet_address`, can access their todos. The key component of the this RLS policy is the expression
```sql
((jwt() ->> 'walletAddress'::text) = wallet_address)
((auth.jwt() ->> 'walletAddress'::text) = wallet_address)
```
This expression checks that the wallet address in the requesting JWT access token is the same as the `wallet_address` in the `todos` table.
+2
View File
@@ -112,6 +112,8 @@ To _permanently_ set a logging configuration (beyond a single session), execute
alter role postgres set pgaudit.log to 'function, write, ddl';
```
For user-traffic flowing through the [HTTP APIs](../../guides/database/api#rest-api-overview) powered by PostgREST, set your configuration values for the roles `anon` and `authenticated`. For traffic using the `service_role` key, set the configuration values for the role `service_role`.
To reset system-wide settings, execute the following, then perform a fast reboot:
```sql
@@ -54,7 +54,7 @@ Nonce means number used once. In reality though, it is a unique and difficult to
## OAuth
OAuth is a protocol allowing third-party applications to request and receive authorization from their users. It is typically used to implement social login, and serves as a base for enterprise single-sign on in the OIDC protocol. Applications can request various level of access, and this includes some user identification information at least (name, email address, user identification number).
OAuth is a protocol allowing third-party applications to request and receive authorization from their users. It is typically used to implement social login, and serves as a base for enterprise single-sign on in the OIDC protocol. Applications can request different levels of access, including basic user identification information such as name, email address, and user ID.
## OIDC
Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

@@ -67,7 +67,7 @@ Dart Edge, built by [Invertase](https://invertase.io/), allows developers to wri
[Check out the guide.](/docs/guides/functions/dart-edge)
### **T3 Turbo x Supabase**
### T3 Turbo x Supabase
T3 Turbo is a type-safe, full-stack template to create Next.js and Expo React Native applications. Using our recent [Mobile Auth updates](/blog/supabase-auth-sso-pkce), the T3 community has helped build the [Supabase Create T3 Turbo template](https://github.com/supabase-community/create-t3-turbo), the fastest and easiest way to get started with Supabase and the T3 Stack. Huge shout out to [Julius](https://twitter.com/jullerino) who single-handedly created the template!
@@ -82,6 +82,17 @@ This Launch Week had a big focus on self-hosting, adding support for self-hosted
- [Digital Ocean](https://supabase-on-do.product-docs.pages.dev/developer-center/hosting-supabase-on-digitalocean/)
- [Stackgres](https://stackgres.io/blog/running-supabase-on-top-of-stackgres/)
<div className="video-container">
<iframe
className="w-full"
src="https://www.youtube-nocookie.com/embed/dDhy6pk282U"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
</div>
We’re looking for more self-hosting maintainers, especially for the self-hosted guides. Everything we do is MIT, Apache2, or PostgreSQL licensed, but our team is relatively small and we don’t have expertise for every deployment strategy that our community might want. If you are willing to help out on a regular basis please reach out!
## Postgres Extensions and improvements
+5
View File
@@ -24,6 +24,8 @@ async function generate() {
'!pages/*/index.tsx',
'!pages/api',
'!pages/404.js',
//get the generated partner pages
'.next/server/pages/partners/*.html',
])
const blogUrl = 'blog'
@@ -37,6 +39,8 @@ async function generate() {
.filter((page) => !page.includes('_document.tsx'))
.map((page) => {
const path = page
//replace the path for the generated partner pages
.replace('.next/server/pages/partners/', '/partners/')
.replace('pages', '')
// add a `/` for blog posts
.replace('_blog', `/${blogUrl}`)
@@ -45,6 +49,7 @@ async function generate() {
.replace('_alternatives', '/alternatives')
.replace('.tsx', '')
.replace('.mdx', '')
.replace('.html', '')
// replace the paths for nested 'index' based routes
.replace('/auth/Auth', '/auth')
.replace('/database/Database', '/database')
+1 -1
View File
@@ -1,4 +1,4 @@
import { Button, IconGrid, IconLayers, IconMenu } from '@supabase/ui'
import { Button, IconGrid, IconLayers, IconMenu } from 'ui'
import ApiExamples from 'data/products/realtime/api-examples'
import AppExamples from 'data/products/realtime/app-examples'
import Solutions from 'data/Solutions.json'
+4
View File
@@ -79,6 +79,10 @@ SUPABASE_PUBLIC_URL=http://localhost:8000
# Enable webp support
IMGPROXY_ENABLE_WEBP_DETECTION=true
############
# Functions - Configuration for Functions
############
FUNCTIONS_HTTP_PORT=9002
############
# Logs - Configuration for Logflare
+10
View File
@@ -81,6 +81,16 @@ services:
syslog-address: "tcp://localhost:${VECTOR_PORT}"
tag: meta
functions:
depends_on:
analytics:
condition: service_healthy
logging:
driver: syslog
options:
syslog-address: "tcp://localhost:${VECTOR_PORT}"
tag: functions
analytics:
container_name: supabase-analytics
image: supabase/logflare:1.0.2
+20
View File
@@ -241,6 +241,26 @@ services:
PG_META_DB_USER: supabase_admin
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
functions:
container_name: supabase-edge-functions
image: supabase/edge-runtime:v1.2.12
restart: unless-stopped
environment:
JWT_SECRET: ${JWT_SECRET}
SUPABASE_URL: http://kong:8000"
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@{POSTGRES_DB}:${POSTGRES_PORT}/${POSTGRES_DB}"
VERIFY_JWT: false
ports:
- ${FUNCTIONS_HTTP_PORT}:9000/tcp
volumes:
- ./volumes/functions:/home/deno/functions:Z
command:
- start
- --main-service
- /home/deno/functions/main
# Comment out everything below this point if you are using an external Postgres database
db:
container_name: supabase-db
+93
View File
@@ -0,0 +1,93 @@
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
console.log('main function started')
const JWT_SECRET = Deno.env.get('JWT_SECRET')
const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'
function getAuthToken(req: Request) {
const authHeader = req.headers.get('authorization')
if (!authHeader) {
throw new Error('Missing authorization header')
}
const [bearer, token] = authHeader.split(' ')
if (bearer !== 'Bearer') {
throw new Error(`Auth header is not 'Bearer {token}'`)
}
return token
}
async function verifyJWT(jwt: string): Promise<boolean> {
const encoder = new TextEncoder()
const secretKey = encoder.encode(JWT_SECRET)
try {
await jose.jwtVerify(jwt, secretKey)
} catch (err) {
console.error(err)
return false
}
return true
}
serve(async (req: Request) => {
if (req.method !== 'OPTIONS' && VERIFY_JWT) {
try {
const token = getAuthToken(req)
const isValidJWT = await verifyJWT(token)
if (!isValidJWT) {
return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
} catch (e) {
console.error(e)
return new Response(JSON.stringify({ msg: e.toString() }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
}
const url = new URL(req.url)
const { pathname } = url
const path_parts = pathname.split('/')
const service_name = path_parts[1]
if (!service_name || service_name === '') {
const error = { msg: 'missing function name in request' }
return new Response(JSON.stringify(error), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
const servicePath = `/home/deno/functions/${service_name}`
console.error(`serving the request with ${servicePath}`)
const memoryLimitMb = 150
const workerTimeoutMs = 1 * 60 * 1000
const noModuleCache = false
const importMapPath = null
const envVarsObj = Deno.env.toObject()
const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])
try {
const worker = await EdgeRuntime.userWorkers.create({
servicePath,
memoryLimitMb,
workerTimeoutMs,
noModuleCache,
importMapPath,
envVars,
})
return await worker.fetch(req)
} catch (e) {
const error = { msg: e.toString() }
return new Response(JSON.stringify(error), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
})
+11
View File
@@ -33,6 +33,7 @@ transforms:
rest: '.appname == "rest"'
realtime: '.appname == "realtime"'
storage: '.appname == "storage"'
functions: '.appname == "functions"'
db: '.appname == "db"'
# Kong logs only include api requests
kong_logs:
@@ -179,6 +180,16 @@ sinks:
# lead to broken queries from studio. This works by the assumption that containers are started in the
# following order: vector > db > logflare > kong
uri: 'http://kong:8000/analytics/v1/api/logs?source_name=postgres.logs&api_key=your-super-secret-and-long-logflare-key'
logflare_functions:
type: 'http'
inputs:
- router.functions
encoding:
codec: 'json'
method: 'post'
request:
retry_max_duration_secs: 10
uri: 'http://analytics:4000/api/logs?source_name=deno-relay-logs&api_key=your-super-secret-and-long-logflare-key'
logflare_storage:
type: 'http'
inputs:
@@ -52,8 +52,6 @@ First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+31 -16
View File
@@ -8,7 +8,7 @@
"name": "nextjs-auth-presence",
"version": "0.1.0",
"dependencies": {
"@supabase/auth-helpers-nextjs": "^0.5.4",
"@supabase/auth-helpers-nextjs": "^0.6.0",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/auth-ui-react": "^0.2.7",
"@supabase/supabase-js": "^2.7.1",
@@ -370,11 +370,11 @@
}
},
"node_modules/@supabase/auth-helpers-nextjs": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.5.4.tgz",
"integrity": "sha512-Xm3BMDOYPbNyuh8UXDRgBDc3JkeM5S2PfuC1jFJ73CCay4YiH1YxTz06Cvtp+vLH432qdjnqH+49ehVvUehPnQ==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.6.0.tgz",
"integrity": "sha512-f1e5blmpt9F/Bnb2hKreWHqf3zEhl29P357d5fnXzQ3Ph7DuW2Wha7uwQVTgE2QH3niqVpJpY0Pg0zBT5NmNWQ==",
"dependencies": {
"@supabase/auth-helpers-shared": "0.2.4"
"@supabase/auth-helpers-shared": "0.3.3"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.0.4"
@@ -389,9 +389,12 @@
}
},
"node_modules/@supabase/auth-helpers-shared": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.2.4.tgz",
"integrity": "sha512-c+hi3N6DhjPTzjXGOokre4gmMZOqt0f1ORrwZwi2s07pk3cbkmn77DwwVtlYMNi3oZNYRQqAfxJu83UzpJrrmg==",
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.3.3.tgz",
"integrity": "sha512-ZwZGffApfyz9MiT3knnZoF1DMWE56H/Q0Mrsn22J9ubhss7/+e7TP3dChxxwlUYqtDmjmLV6OV8W0BANENfUew==",
"dependencies": {
"js-base64": "^3.7.5"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.0.4"
}
@@ -2206,6 +2209,11 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/js-base64": {
"version": "3.7.5",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz",
"integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA=="
},
"node_modules/js-sdsl": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
@@ -3603,11 +3611,11 @@
"requires": {}
},
"@supabase/auth-helpers-nextjs": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.5.4.tgz",
"integrity": "sha512-Xm3BMDOYPbNyuh8UXDRgBDc3JkeM5S2PfuC1jFJ73CCay4YiH1YxTz06Cvtp+vLH432qdjnqH+49ehVvUehPnQ==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.6.0.tgz",
"integrity": "sha512-f1e5blmpt9F/Bnb2hKreWHqf3zEhl29P357d5fnXzQ3Ph7DuW2Wha7uwQVTgE2QH3niqVpJpY0Pg0zBT5NmNWQ==",
"requires": {
"@supabase/auth-helpers-shared": "0.2.4"
"@supabase/auth-helpers-shared": "0.3.3"
}
},
"@supabase/auth-helpers-react": {
@@ -3617,10 +3625,12 @@
"requires": {}
},
"@supabase/auth-helpers-shared": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.2.4.tgz",
"integrity": "sha512-c+hi3N6DhjPTzjXGOokre4gmMZOqt0f1ORrwZwi2s07pk3cbkmn77DwwVtlYMNi3oZNYRQqAfxJu83UzpJrrmg==",
"requires": {}
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.3.3.tgz",
"integrity": "sha512-ZwZGffApfyz9MiT3knnZoF1DMWE56H/Q0Mrsn22J9ubhss7/+e7TP3dChxxwlUYqtDmjmLV6OV8W0BANENfUew==",
"requires": {
"js-base64": "^3.7.5"
}
},
"@supabase/auth-ui-react": {
"version": "0.2.7",
@@ -4975,6 +4985,11 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"js-base64": {
"version": "3.7.5",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz",
"integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA=="
},
"js-sdsl": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
@@ -10,7 +10,7 @@
"format": "prettier --write \"pages/**/*.{js,jsx,ts,tsx,css,md,json}\""
},
"dependencies": {
"@supabase/auth-helpers-nextjs": "^0.5.4",
"@supabase/auth-helpers-nextjs": "^0.6.0",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/auth-ui-react": "^0.2.7",
"@supabase/supabase-js": "^2.7.1",
File diff suppressed because it is too large. Load diff
@@ -62,9 +62,9 @@ export const useStore = (props) => {
.subscribe()
// Cleanup on unmount
return () => {
supabase.removeChannel('public:messages')
supabase.removeChannel('public:users')
supabase.removeChannel('public:channels')
supabase.removeChannel(supabase.channel(messageListener))
supabase.removeChannel(supabase.channel(userListener))
supabase.removeChannel(supabase.channel(channelListener))
}
}, [])
@@ -179,7 +179,7 @@ export const fetchMessages = async (channelId, setState) => {
.from('messages')
.select(`*, author:user_id(*)`)
.eq('channel_id', channelId)
.order('inserted_at', true)
.order('inserted_at', { ascending: true })
if (setState) setState(data)
return data
} catch (error) {
@@ -12,17 +12,10 @@ export default function SupabaseSlackClone({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
supabase.auth.getSession().then(({ data: { session }}) => {
setSession(session)
setUserLoaded(session ? true : false)
if (session?.user) {
signIn()
router.push('/channels/[id]', '/channels/1')
}
})
const { subscription: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
function saveSession(
/** @type {Awaited<ReturnType<typeof supabase.auth.getSession>>['data']['session']} */
session
) {
setSession(session)
const currentUser = session?.user
setUser(currentUser ?? null)
@@ -31,7 +24,11 @@ export default function SupabaseSlackClone({ Component, pageProps }) {
signIn(currentUser.id, currentUser.email)
router.push('/channels/[id]', '/channels/1')
}
})
}
supabase.auth.getSession().then(({ data: { session }}) => saveSession(session))
const { subscription: authListener } = supabase.auth.onAuthStateChange(async (event, session) => saveSession(session))
return () => {
authListener.unsubscribe()
@@ -17,23 +17,31 @@ export default function Avatar({
const supabase = useSupabaseClient<Database>()
const [avatarUrl, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
const [uploading, setUploading] = useState(false)
const [blobUrlCache, setBlobUrlCache] = useState<Record<string, string>>({})
useEffect(() => {
async function downloadImage(path: string) {
try {
if (blobUrlCache[path]) {
setAvatarUrl(blobUrlCache[path])
return
}
const { data, error } = await supabase.storage.from('avatars').download(path)
if (error) {
throw error
}
const url = URL.createObjectURL(data)
setAvatarUrl(url)
setBlobUrlCache((prevCache) => ({ ...prevCache, [path]: url }))
} catch (error) {
console.log('Error downloading image: ', error)
}
}
if (url) downloadImage(url)
}, [url, supabase.storage])
}, [url, supabase.storage, blobUrlCache])
const uploadAvatar: React.ChangeEventHandler<HTMLInputElement> = async (event) => {
try {
+3 -1
View File
@@ -38843,7 +38843,8 @@
"dependencies": {
"@headlessui/react": "^1.5.0",
"@supabase/gotrue-js": "^2.16.0",
"@supabase/ui": "^0.37.0-alpha.50"
"@supabase/ui": "^0.37.0-alpha.50",
"react-use": "^17.4.0"
},
"devDependencies": {
"@types/react": "^17.0.37",
@@ -49557,6 +49558,7 @@
"@types/react": "^17.0.37",
"@types/react-dom": "^17.0.11",
"config": "*",
"react-use": "*",
"tsconfig": "*",
"typescript": "^4.5.3"
},
+2 -1
View File
@@ -7,7 +7,8 @@
"dependencies": {
"@headlessui/react": "^1.5.0",
"@supabase/gotrue-js": "^2.16.0",
"@supabase/ui": "^0.37.0-alpha.50"
"@supabase/ui": "^0.37.0-alpha.50",
"react-use": "^17.4.0"
},
"devDependencies": {
"@types/react": "^17.0.37",
+36 -27
View File
@@ -6,7 +6,22 @@ const brandColors = require('./default-colors')
const { default: flattenColorPalette } = require('tailwindcss/lib/util/flattenColorPalette')
// console.log(Object.keys(radixUiColors))
// exclude these colors from the included set from Radix
const excludedRadixColors = [
'bronze',
'brown',
'cyan',
'grass',
'olive',
'mauve',
'mint',
'lime',
'plum',
'sage',
'sand',
'sky',
'teal',
]
// generates fixed scales
// based on the root/light mode version
@@ -26,7 +41,11 @@ function radixColorKeys() {
keys = filterItems(keys, 'Dark')
// console.log('radixColorKeys', keys)
// remove excluded colors
keys = keys.filter(
(key) => !excludedRadixColors.some((excludeColor) => key.startsWith(excludeColor))
)
return keys
}
@@ -41,11 +60,7 @@ function generateColorClasses() {
// create empty obj for each color
mappedColors[x] = {}
// create empty obj for each fixed color
if (
fixedOptions.some(function (v) {
return x.indexOf(v) >= 0
})
) {
if (fixedOptions.some((v) => x.indexOf(v) >= 0)) {
mappedColors[`${x}-fixed`] = {}
}
})
@@ -55,12 +70,7 @@ function generateColorClasses() {
const step = index + 1
mappedColors[x][step * 100] = `var(--colors-${x}${step})`
if (
fixedOptions.some(function (v) {
return x.indexOf(v) >= 0
})
) {
// console.log(x)
if (fixedOptions.some((v) => x.indexOf(v) >= 0)) {
mappedColors[`${x}-fixed`][step * 100] = `var(--colors-fixed-${x}${step})`
}
}
@@ -71,13 +81,7 @@ function generateColorClasses() {
const colorClasses = generateColorClasses()
/*
* generateCssVariables()
*
* generate the CSS variables for tailwind to use
*
*/
// generate the CSS variables for tailwind to use
function generateCssVariables() {
// potential options
// { fixedOptions, brandColors }
@@ -85,7 +89,12 @@ function generateCssVariables() {
let rootColors = {}
let darkColors = {}
const radixArray = Object.values(radixUiColors)
const radixArray = Object.entries(radixUiColors)
.filter(
([key, value]) => !excludedRadixColors.some((excludeColor) => key.startsWith(excludeColor))
)
.map(([, value]) => value)
const brandArray = Object.values(brandColors)
function generateColors(colors, index, colorSet) {
@@ -379,12 +388,12 @@ const uiConfig = {
bordershadow: (value) => {
return {
boxShadow: `
var(--colors-blackA1) 0px 0px 0px 0px,
var(--colors-blackA1) 0px 0px 0px 0px,
var(--colors-blackA8) 0px 1px 1px 0px,
${value} 0px 0px 0px 1px,
var(--colors-blackA1) 0px 0px 0px 0px,
var(--colors-blackA1) 0px 0px 0px 0px,
var(--colors-blackA1) 0px 0px 0px 0px,
var(--colors-blackA1) 0px 0px 0px 0px,
var(--colors-blackA8) 0px 1px 1px 0px,
${value} 0px 0px 0px 1px,
var(--colors-blackA1) 0px 0px 0px 0px,
var(--colors-blackA1) 0px 0px 0px 0px,
rgb(64 68 82 / 8%) 0px 2px 5px 0px;
`,
}
+4 -158
View File
@@ -1,159 +1,13 @@
const deepMerge = require('deepmerge')
const customFormsPlugin = require('@tailwindcss/forms')
const colors = {
transparent: 'transparent',
white: '#ffffff',
black: '#000000',
gray: {
'50': '#f9fafb',
'100': '#f4f5f7',
'200': '#e5e7eb',
'300': '#d5d6d7',
'400': '#9e9e9e',
'500': '#707275',
'600': '#4c4f52',
'700': '#24262d',
'800': '#1a1c23',
'900': '#121317',
// default values from Tailwind UI palette
// '300': '#d2d6dc',
// '400': '#9fa6b2',
// '500': '#6b7280',
// '600': '#4b5563',
// '700': '#374151',
// '800': '#252f3f',
// '900': '#161e2e',
},
'cool-gray': {
'50': '#fbfdfe',
'100': '#f1f5f9',
'200': '#e2e8f0',
'300': '#cfd8e3',
'400': '#97a6ba',
'500': '#64748b',
'600': '#475569',
'700': '#364152',
'800': '#27303f',
'900': '#1a202e',
},
red: {
'50': '#fdf2f2',
'100': '#fde8e8',
'200': '#fbd5d5',
'300': '#f8b4b4',
'400': '#f98080',
'500': '#f05252',
'600': '#e02424',
'700': '#c81e1e',
'800': '#9b1c1c',
'900': '#771d1d',
},
orange: {
'50': '#fff8f1',
'100': '#feecdc',
'200': '#fcd9bd',
'300': '#fdba8c',
'400': '#ff8a4c',
'500': '#ff5a1f',
'600': '#d03801',
'700': '#b43403',
'800': '#8a2c0d',
'900': '#771d1d',
},
yellow: {
'50': '#fdfdea',
'100': '#fdf6b2',
'200': '#fce96a',
'300': '#faca15',
'400': '#e3a008',
'500': '#c27803',
'600': '#9f580a',
'700': '#8e4b10',
'800': '#723b13',
'900': '#633112',
},
green: {
'50': '#f3faf7',
'100': '#def7ec',
'200': '#bcf0da',
'300': '#84e1bc',
'400': '#31c48d',
'500': '#0e9f6e',
'600': '#057a55',
'700': '#046c4e',
'800': '#03543f',
'900': '#014737',
},
teal: {
'50': '#edfafa',
'100': '#d5f5f6',
'200': '#afecef',
'300': '#7edce2',
'400': '#16bdca',
'500': '#0694a2',
'600': '#047481',
'700': '#036672',
'800': '#05505c',
'900': '#014451',
},
blue: {
'50': '#ebf5ff',
'100': '#e1effe',
'200': '#c3ddfd',
'300': '#a4cafe',
'400': '#76a9fa',
'500': '#3f83f8',
'600': '#1c64f2',
'700': '#1a56db',
'800': '#1e429f',
'900': '#233876',
},
indigo: {
'50': '#f0f5ff',
'100': '#e5edff',
'200': '#cddbfe',
'300': '#b4c6fc',
'400': '#8da2fb',
'500': '#6875f5',
'600': '#5850ec',
'700': '#5145cd',
'800': '#42389d',
'900': '#362f78',
},
purple: {
'50': '#f6f5ff',
'100': '#edebfe',
'200': '#dcd7fe',
'300': '#cabffd',
'400': '#ac94fa',
'500': '#9061f9',
'600': '#7e3af2',
'700': '#6c2bd9',
'800': '#5521b5',
'900': '#4a1d96',
},
pink: {
'50': '#fdf2f8',
'100': '#fce8f3',
'200': '#fad1e8',
'300': '#f8b4d9',
'400': '#f17eb8',
'500': '#e74694',
'600': '#d61f69',
'700': '#bf125d',
'800': '#99154b',
'900': '#751a3d',
},
}
const backgroundOpacity = (theme) => ({
'10': '0.1',
10: '0.1',
...theme('opacity'),
})
const maxHeight = (theme) => ({
'0': '0',
0: '0',
xl: '36rem',
...theme('spacing'),
})
@@ -162,12 +16,11 @@ const windmillConfig = {
darkMode: 'class',
purge: {
content: [
'node_modules/@supabase/ui/theme/defaultTheme.js',
//'node_modules/@supabase/ui/theme/defaultTheme.js',
// 'node_modules/@supabase/ui/dist/index.js',
],
},
theme: {
colors,
backgroundOpacity,
maxHeight,
},
@@ -175,14 +28,7 @@ const windmillConfig = {
backgroundOpacity: ['responsive', 'hover', 'focus', 'dark'],
backgroundColor: ['responsive', 'hover', 'focus', 'active', 'odd', 'dark'],
display: ['responsive', 'dark'],
textColor: [
'responsive',
'focus',
'focus-within',
'hover',
'active',
'dark',
],
textColor: ['responsive', 'focus', 'focus-within', 'hover', 'active', 'dark'],
placeholderColor: ['responsive', 'focus', 'dark'],
borderColor: ['responsive', 'hover', 'focus', 'dark'],
divideColor: ['responsive', 'dark'],
-90
View File
@@ -1,90 +0,0 @@
module.exports = {
brand: {
brand1: 'hsla(153, 80%, 99%, 1)',
brand2: 'hsla(148, 88%, 97%, 1)',
brand3: 'hsla(148, 76%, 95%, 1)',
brand4: 'hsla(148, 68%, 92%, 1)',
brand5: 'hsla(148, 59%, 88%, 1)',
brand6: 'hsla(149, 43%, 82%, 1)',
brand7: 'hsla(149, 43%, 69%, 1)',
brand8: 'hsla(154, 55%, 45%, 1)',
brand9: 'hsla(153, 60%, 53%, 1)',
brand10: 'hsla(153, 50%, 50%, 1)',
brand11: 'hsla(153, 50%, 34%, 1)',
brand12: 'hsla(153, 40%, 12%, 1)',
},
brandDark: {
brand1: 'hsla(153, 75%, 6%, 1)',
brand2: 'hsla(153, 73%, 7%, 1)',
brand3: 'hsla(154, 69%, 9%, 1)',
brand4: 'hsla(154, 67%, 11%, 1)',
brand5: 'hsla(154, 66%, 13%, 1)',
brand6: 'hsla(154, 64%, 17%, 1)',
brand7: 'hsla(154, 62%, 22%, 1)',
brand8: 'hsla(153, 60%, 28%, 1)',
brand9: 'hsla(153, 60%, 53%, 1)',
brand10: 'hsla(153, 60%, 70%, 1)',
brand11: 'hsla(153, 60%, 50%, 1)',
brand12: 'hsla(153, 60%, 95%, 1)',
},
contrast: {
'brand-hiContrast': 'var(--colors-brand-9)',
'brand-loContrast': 'var(--colors-brand-4)',
},
scale: {
scale1: 'var(--colors-slate1)',
scale2: 'var(--colors-slate2)',
scale3: 'var(--colors-slate3)',
scale4: 'var(--colors-slate4)',
scale5: 'var(--colors-slate5)',
scale6: 'var(--colors-slate6)',
scale7: 'var(--colors-slate7)',
scale8: 'var(--colors-slate8)',
scale9: 'var(--colors-slate9)',
scale10: 'var(--colors-slate10)',
scale11: 'var(--colors-slate11)',
scale12: 'var(--colors-slate12)',
},
scaleA: {
scaleA1: 'var(--colors-slatea1)',
scaleA2: 'var(--colors-slatea2)',
scaleA3: 'var(--colors-slatea3)',
scaleA4: 'var(--colors-slatea4)',
scaleA5: 'var(--colors-slatea5)',
scaleA6: 'var(--colors-slatea6)',
scaleA7: 'var(--colors-slatea7)',
scaleA8: 'var(--colors-slatea8)',
scaleA9: 'var(--colors-slatea9)',
scaleA10: 'var(--colors-slatea10)',
scaleA11: 'var(--colors-slatea11)',
scaleA12: 'var(--colors-slatea12)',
},
scaleDark: {
scale1: 'var(--colors-gray1)',
scale2: 'var(--colors-gray2)',
scale3: 'var(--colors-gray3)',
scale4: 'var(--colors-gray4)',
scale5: 'var(--colors-gray5)',
scale6: 'var(--colors-gray6)',
scale7: 'var(--colors-gray7)',
scale8: 'var(--colors-gray8)',
scale9: 'var(--colors-gray9)',
scale10: 'var(--colors-gray10)',
scale11: 'var(--colors-gray11)',
scale12: 'var(--colors-gray12)',
},
scaleADark: {
scaleA1: 'var(--colors-graya1)',
scaleA2: 'var(--colors-graya2)',
scaleA3: 'var(--colors-graya3)',
scaleA4: 'var(--colors-graya4)',
scaleA5: 'var(--colors-graya5)',
scaleA6: 'var(--colors-graya6)',
scaleA7: 'var(--colors-graya7)',
scaleA8: 'var(--colors-graya8)',
scaleA9: 'var(--colors-graya9)',
scaleA10: 'var(--colors-graya10)',
scaleA11: 'var(--colors-graya11)',
scaleA12: 'var(--colors-graya12)',
},
}
File diff suppressed because it is too large. Load diff
@@ -1,3 +1,4 @@
import { COLORS } from '../../lib/constants'
import { Badge } from './'
export default {
@@ -5,39 +6,6 @@ export default {
component: Badge,
}
const colors = [
'brand',
'scale',
'tomato',
'red',
'crimson',
'pink',
'plum',
'purple',
'violet',
'indigo',
'blue',
'cyan',
'teal',
'green',
'grass',
'brown',
'orange',
'sky',
'mint',
'lime',
'yellow',
'amber',
'gold',
'bronze',
'gray',
'mauve',
'slate',
'sage',
'olive',
'sand',
]
const sizes: string[] = ['small', 'large']
export const Default = (args: any) => <Badge {...args}>Hello world</Badge>
@@ -50,9 +18,7 @@ export const large = (args: any) => <Badge {...args}>Hello world</Badge>
export const withDotLarge = (args: any) => <Badge {...args}>Hello world</Badge>
export const withCustomClassNames = (args: any) => (
<Badge {...args}>Hello world</Badge>
)
export const withCustomClassNames = (args: any) => <Badge {...args}>Hello world</Badge>
export const allBadges = () => (
<>
@@ -61,7 +27,7 @@ export const allBadges = () => (
<>
<h3 className="text-scale-900">{size}</h3>
<div className="flex flex-col gap-2">
{colors.map((x, colorIndex) => (
{COLORS.map((x, colorIndex) => (
// @ts-ignore
<Badge size={sizes[sizeIndex]} color={colors[colorIndex]}>
Supabase
@@ -94,5 +60,5 @@ withDotLarge.args = {
}
withCustomClassNames.args = {
className: 'border-teal-100 border-2'
className: 'border-teal-100 border-2',
}
+2 -31
View File
@@ -1,37 +1,8 @@
import { AvailableColors } from '../../lib/constants'
import styleHandler from '../../lib/theme/styleHandler'
interface Props {
color?:
| 'brand'
| 'scale'
| 'tomato'
| 'red'
| 'crimson'
| 'pink'
| 'plum'
| 'purple'
| 'violet'
| 'indigo'
| 'blue'
| 'cyan'
| 'teal'
| 'green'
| 'grass'
| 'brown'
| 'orange'
| 'sky'
| 'mint'
| 'lime'
| 'yellow'
| 'amber'
| 'gold'
| 'bronze'
| 'gray'
| 'mauve'
| 'slate'
| 'sage'
| 'olive'
| 'sand'
color?: AvailableColors
children: string | React.ReactNode
size?: 'large' | 'small'
dot?: boolean
+41
View File
@@ -1 +1,42 @@
export const HIDDEN_PLACEHOLDER = '**** **** **** ****'
export const COLORS = [
'brand',
'scale',
'tomato',
'red',
'crimson',
'pink',
'purple',
'violet',
'indigo',
'blue',
'green',
'grass',
'orange',
'sky',
'yellow',
'amber',
'gold',
'gray',
'slate',
]
export type AvailableColors =
| 'brand'
| 'scale'
| 'tomato'
| 'red'
| 'crimson'
| 'pink'
| 'purple'
| 'violet'
| 'indigo'
| 'blue'
| 'green'
| 'grass'
| 'orange'
| 'sky'
| 'yellow'
| 'amber'
| 'gold'
| 'gray'
| 'slate'
-12
View File
@@ -222,30 +222,18 @@ export default {
red: `bg-red-200 text-red-1100 border border-red-700`,
crimson: `bg-crimson-200 text-crimson-1100 border border-crimson-700`,
pink: `bg-pink-200 text-pink-1100 border border-pink-700`,
plum: `bg-plum-200 text-plum-1100 border border-plum-700`,
purple: `bg-purple-200 text-purple-1100 border border-purple-700`,
violet: `bg-violet-200 text-violet-1100 border border-violet-700`,
indigo: `bg-indigo-200 text-indigo-1100 border border-indigo-700`,
blue: `bg-blue-200 text-blue-1100 border border-blue-700`,
cyan: `bg-cyan-200 text-cyan-1100 border border-cyan-700`,
teal: `bg-teal-200 text-teal-1100 border border-teal-700`,
green: `bg-green-200 text-green-1100 border border-green-700`,
grass: `bg-grass-200 text-grass-1100 border border-grass-700`,
brown: `bg-brown-200 text-brown-1100 border border-brown-700`,
orange: `bg-orange-200 text-orange-1100 border border-orange-700`,
sky: `bg-sky-200 text-sky-1100 border border-sky-700`,
mint: `bg-mint-200 text-mint-1100 border border-mint-700`,
lime: `bg-lime-200 text-lime-1100 border border-lime-700`,
yellow: `bg-yellow-200 text-yellow-1100 border border-yellow-700`,
amber: `bg-amber-200 text-amber-1100 border border-amber-700`,
gold: `bg-gold-200 text-gold-1100 border border-gold-700`,
bronze: `bg-bronze-200 text-bronze-1100 border border-bronze-700`,
gray: `bg-gray-200 text-gray-1100 border border-gray-700`,
mauve: `bg-mauve-200 text-mauve-1100 border border-mauve-700`,
slate: `bg-slate-200 text-slate-1100 border border-slate-700`,
sage: `bg-sage-200 text-sage-1100 border border-sage-700`,
olive: `bg-olive-200 text-olive-1100 border border-olive-700`,
sand: `bg-sand-200 text-sand-1100 border border-sand-700`,
},
},
+1 -477
View File
@@ -1,477 +1 @@
const gray = {
100: '#eeeeee',
200: '#e0e0e0',
300: '#bbbbbb',
400: '#666666',
500: '#444444',
650: '#333',
600: '#2a2a2a',
700: '#1f1f1f',
800: '#181818',
900: '#0f0f0f',
}
const brand = {
100: '#c5f1dd',
200: '#c5f1dd',
300: '#9fe7c7',
400: '#65d9a5',
500: '#24b47e',
600: '#38bc81',
700: '#1c8656',
800: '#10633e',
900: '#10633e',
}
const blueGray = {
50: '#F8FAFC',
100: '#F1F5F9',
200: '#E2E8F0',
300: '#CBD5E1',
400: '#94A3B8',
500: '#64748B',
600: '#475569',
700: '#334155',
800: '#1E293B',
900: '#0F172A',
}
const coolGray = {
50: '#F9FAFB',
100: '#F3F4F6',
200: '#E5E7EB',
300: '#D1D5DB',
400: '#9CA3AF',
500: '#6B7280',
600: '#4B5563',
700: '#374151',
800: '#1F2937',
900: '#111827',
}
const colors = {
// transparent: 'transparent',
// white: '#ffffff',
// black: '#000000',
// gray: {
// '50': '#f9fafb',
// '100': '#f4f5f7',
// '200': '#e5e7eb',
// '300': '#d5d6d7',
// '400': '#9e9e9e',
// '500': '#707275',
// '600': '#4c4f52',
// '700': '#24262d',
// '800': '#1a1c23',
// '900': '#121317',
// // default values from Tailwind UI palette
// // '300': '#d2d6dc',
// // '400': '#9fa6b2',
// // '500': '#6b7280',
// // '600': '#4b5563',
// // '700': '#374151',
// // '800': '#252f3f',
// // '900': '#161e2e',
// },
red: {
50: '#fdf2f2',
100: '#fde8e8',
200: '#fbd5d5',
300: '#f8b4b4',
400: '#f98080',
500: '#f05252',
600: '#e02424',
700: '#c81e1e',
800: '#9b1c1c',
900: '#771d1d',
},
orange: {
50: '#fff8f1',
100: '#feecdc',
200: '#fcd9bd',
300: '#fdba8c',
400: '#ff8a4c',
500: '#ff5a1f',
600: '#d03801',
700: '#b43403',
800: '#8a2c0d',
900: '#771d1d',
},
yellow: {
50: '#fdfdea',
100: '#fdf6b2',
200: '#fce96a',
300: '#faca15',
400: '#e3a008',
500: '#c27803',
600: '#9f580a',
700: '#8e4b10',
800: '#723b13',
900: '#633112',
},
green: {
50: '#f3faf7',
100: '#def7ec',
200: '#bcf0da',
300: '#84e1bc',
400: '#31c48d',
500: '#0e9f6e',
600: '#057a55',
700: '#046c4e',
800: '#03543f',
900: '#014737',
},
teal: {
50: '#edfafa',
100: '#d5f5f6',
200: '#afecef',
300: '#7edce2',
400: '#16bdca',
500: '#0694a2',
600: '#047481',
700: '#036672',
800: '#05505c',
900: '#014451',
},
blue: {
50: '#ebf5ff',
100: '#e1effe',
200: '#c3ddfd',
300: '#a4cafe',
400: '#76a9fa',
500: '#3f83f8',
600: '#1c64f2',
700: '#1a56db',
800: '#1e429f',
900: '#233876',
},
indigo: {
50: '#f0f5ff',
100: '#e5edff',
200: '#cddbfe',
300: '#b4c6fc',
400: '#8da2fb',
500: '#6875f5',
600: '#5850ec',
700: '#5145cd',
800: '#42389d',
900: '#362f78',
},
purple: {
50: '#f6f5ff',
100: '#edebfe',
200: '#dcd7fe',
300: '#cabffd',
400: '#ac94fa',
500: '#9061f9',
600: '#7e3af2',
700: '#6c2bd9',
800: '#5521b5',
900: '#4a1d96',
},
pink: {
50: '#fdf2f8',
100: '#fce8f3',
200: '#fad1e8',
300: '#f8b4d9',
400: '#f17eb8',
500: '#e74694',
600: '#d61f69',
700: '#bf125d',
800: '#99154b',
900: '#751a3d',
},
}
const dark = {
50: '#eeeeee',
100: '#eeeeee',
200: '#e0e0e0',
300: '#bbbbbb',
400: '#666666',
500: '#444444',
600: '#2a2a2a',
700: '#1f1f1f',
800: '#181818',
900: '#0f0f0f',
}
const sbui_border = {
100: 'var(--border-1)',
200: 'var(--border-2)',
300: 'var(--border-3)',
400: 'var(--border-4)',
500: 'var(--border-5)',
600: 'var(--border-6)',
700: 'var(--border-7)',
800: 'var(--border-8)',
900: 'var(--border-9)',
1000: 'var(--border-10)',
1100: 'var(--border-11)',
1200: 'var(--border-12)',
}
const sbui_background = {
100: 'var(--background-1)',
200: 'var(--background-2)',
300: 'var(--background-3)',
400: 'var(--background-4)',
500: 'var(--background-5)',
600: 'var(--background-6)',
700: 'var(--background-7)',
800: 'var(--background-8)',
900: 'var(--background-9)',
1000: 'var(--background-10)',
1100: 'var(--background-11)',
1200: 'var(--background-12)',
}
const sbui_brand = {
100: 'var(--brand-1)',
200: 'var(--brand-2)',
300: 'var(--brand-3)',
400: 'var(--brand-4)',
500: 'var(--brand-5)',
600: 'var(--brand-6)',
700: 'var(--brand-7)',
800: 'var(--brand-8)',
900: 'var(--brand-9)',
1000: 'var(--brand-10)',
1100: 'var(--brand-11)',
1200: 'var(--brand-12)',
}
const sbui_brand_fixed = {
100: 'var(--brand-fixed-1)',
200: 'var(--brand-fixed-2)',
300: 'var(--brand-fixed-3)',
400: 'var(--brand-fixed-4)',
500: 'var(--brand-fixed-5)',
600: 'var(--brand-fixed-6)',
700: 'var(--brand-fixed-7)',
800: 'var(--brand-fixed-8)',
900: 'var(--brand-fixed-9)',
1000: 'var(--brand-fixed-10)',
1100: 'var(--brand-fixed-11)',
1200: 'var(--brand-fixed-12)',
}
function generateRadixVariables() {
const colors = [
'brand',
'brand-fixed',
'scale',
'tomato',
'red',
'crimson',
'pink',
'plum',
'purple',
'violet',
'indigo',
'blue',
'cyan',
'teal',
'green',
'grass',
'brown',
'orange',
'sky',
'mint',
'lime',
'yellow',
'amber',
'gold',
'bronze',
'gray',
'mauve',
'slate',
'sage',
'olive',
'sand',
]
let mappedColors = {}
colors.map((x) => {
mappedColors[x] = {}
})
colors.map((x) => {
for (let index = 0; index < 12; index++) {
const step = index + 1
mappedColors[x][step * 100] = `var(--colors-${x}${step})`
}
})
return mappedColors
}
const custom_colors = generateRadixVariables()
// console.log('radix colors', custom_colors)
const ui = require('./ui.config.js')
module.exports = ui({
content: {
// enabled: true,
content: [
'./src/**/**/*.{js,ts,jsx,tsx,html,mdx}',
'./src/**/*.{js,ts,jsx,tsx,html,mdx}',
'./src/**/*.{js,ts,jsx,tsx,html,mdx}',
'./src/lib/theme/defaultTheme.ts',
],
// safelist: ['space-x-1'],
},
darkMode: 'class',
theme: {
// colors: {
// gray: { ...custom_colors.gray },
// slate: { ...custom_colors.slate },
// },
extend: {
typography: {
DEFAULT: {
css: {
'code::before': {
content: '""',
},
'code::after': {
content: '""',
},
},
},
},
colors: {
/*
main
*/
['brandColor']: '#EC6F0D',
['brandScale']: { ...sbui_brand },
['brandScaleFixed']: { ...sbui_brand_fixed },
// used for backgrounds
['bgScale']: { ...sbui_background },
['bgScaleDark']: { ...dark },
// used for borders, inputs and text
['grayScale']: { ...sbui_border },
['grayScaleDark']: { ...gray },
/*
typography
*/
'typography-body': {
light: coolGray[600],
dark: gray[100],
},
'typography-body-secondary': {
light: coolGray[500],
dark: gray[300],
},
'typography-body-strong': {
light: coolGray[100],
dark: 'white',
},
'typography-body-faded': {
light: coolGray[400],
dark: gray[400],
},
/*
app backgrounds
*/
'bg-primary': {
light: 'white',
dark: gray[800],
},
'bg-secondary': {
light: blueGray[100],
dark: gray[700],
},
'bg-alt': {
light: blueGray[50], // gray[100],
dark: gray[600],
},
/*
Forms
*/
'input-value': {
light: coolGray[600],
dark: gray[200],
},
'input-placeholder': {
light: coolGray[300],
dark: gray[400],
},
'input-border': {
light: coolGray[300],
dark: gray[500],
},
'input-label': {
light: coolGray[600],
dark: gray[200],
},
'input-border-hover': {
light: coolGray[400],
dark: gray[400],
},
'input-border-focus': {
light: brand[300],
dark: brand[300],
},
// ['brandColor']: '#7c3aed',
transparent: 'transparent',
current: 'currentColor',
black: '#000',
white: '#fff',
// gray: { ...gray },
// brand: { ...brand },
// blueGray: { ...blueGray },
// coolGray: { ...coolGray },
dark: {
100: '#eeeeee',
200: '#e0e0e0',
300: '#bbbbbb',
400: '#666666',
500: '#444444',
600: '#2a2a2a',
700: '#1f1f1f',
800: '#181818',
900: '#0f0f0f',
},
'secondary-text': {
light: '#71767F',
dark: '#878787',
},
// brand: {
// 50: '#82dab0',
// 100: '#82dab0',
// 200: '#69d3a0',
// 300: '#50cb90',
// 400: '#C5F1DD',
// 500: '#9FE7C7',
// 600: '#65D9A5',
// 700: '#3ECF8E',
// 800: '#24b47e', // green-500 in dashboard
// 900: '#2c9c6a',
// },
// ...custom_colors,
},
// transitionProperty: {
// 'max-height': 'max-height',
// },
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
})
module.exports = require('config/tailwind.config')
-399
View File
@@ -1,399 +0,0 @@
const deepMerge = require('deepmerge')
const forms = require('@tailwindcss/forms')
const plugin = require('tailwindcss/plugin')
const radixUiColors = require('@radix-ui/colors')
const brandColors = require('./default-colors')
const { default: flattenColorPalette } = require('tailwindcss/lib/util/flattenColorPalette')
// console.log(Object.keys(radixUiColors))
// generates fixed scales
// based on the root/light mode version
const fixedOptions = ['scale', 'scaleA', 'brand']
function radixColorKeys() {
let keys = Object.keys(radixUiColors)
/**
* Filter array items based on search criteria (query)
*/
function filterItems(arr, query) {
return arr.filter(function (el) {
return el.toLowerCase().indexOf(query.toLowerCase()) == -1
})
}
keys = filterItems(keys, 'Dark')
// console.log('radixColorKeys', keys)
return keys
}
function generateColorClasses() {
const brandColors = ['brand', 'scale', 'scaleA']
const colors = [...radixColorKeys(), ...brandColors]
let mappedColors = {}
// generate the shape of the colors object
colors.map((x) => {
// create empty obj for each color
mappedColors[x] = {}
// create empty obj for each fixed color
if (
fixedOptions.some(function (v) {
return x.indexOf(v) >= 0
})
) {
mappedColors[`${x}-fixed`] = {}
}
})
colors.map((x) => {
for (let index = 0; index < 12; index++) {
const step = index + 1
mappedColors[x][step * 100] = `var(--colors-${x.toLowerCase()}${step})`
if (
fixedOptions.some(function (v) {
return x.indexOf(v) >= 0
})
) {
// console.log(x)
mappedColors[`${x}-fixed`][step * 100] = `var(--colors-fixed-${x}${step})`
}
}
})
return mappedColors
}
const colorClasses = generateColorClasses()
/*
* generateCssVariables()
*
* generate the CSS variables for tailwind to use
*
*/
function generateCssVariables() {
// potential options
// { fixedOptions, brandColors }
let rootColors = {}
let darkColors = {}
const radixArray = Object.values(radixUiColors)
const brandArray = Object.values(brandColors)
function generateColors(colors, index, colorSet) {
const key = Object.keys(colorSet)[index]
if (key.includes('Dark')) {
darkColors = { ...darkColors, ...colors }
} else {
rootColors = { ...rootColors, ...colors }
// generate an optional 'fixed' scale of colors
if (
fixedOptions.some(function (v) {
return key.indexOf(v) >= 0
})
) {
rootColors.fixed = { ...rootColors?.fixed, ...colors }
}
}
}
radixArray.map((x, i) => {
generateColors(x, i, radixUiColors)
})
brandArray.map((x, i) => {
generateColors(x, i, brandColors)
})
return {
root: { ...rootColors },
dark: { ...darkColors },
}
}
const variables = generateCssVariables()
const uiConfig = {
theme: {
variables: {
DEFAULT: {
width: {
listbox: '320px',
},
colors: { ...variables.root },
},
'.dark': {
colors: { ...variables.dark },
},
},
extend: {
// dropdown extensions
transformOrigin: {
// tailwind class for this is `origin-dropdown`
dropdown: 'var(--radix-dropdown-menu-content-transform-origin)',
popover: 'var(--radix-popover-menu-content-transform-origin);',
},
width: {
listbox: 'var(--width-listbox);',
},
keyframes: {
fadeIn: {
'0%': { transform: 'scale(0.95)', opacity: 0 },
'100%': { transform: 'scale(1)', opacity: 1 },
},
fadeOut: {
'0%': { transform: 'scale(1)', opacity: 1 },
'100%': { transform: 'scale(0.95)', opacity: 0 },
},
overlayContentShow: {
'0%': { opacity: 0, transform: 'translate(0%, -2%) scale(.96)' },
'100%': { opacity: 1, transform: 'translate(0%, 0%) scale(1)' },
},
overlayContentHide: {
'0%': { opacity: 1, transform: 'translate(0%, 0%) scale(1)' },
'100%': { opacity: 0, transform: 'translate(0%, -2%) scale(.96)' },
},
dropdownFadeIn: {
'0%': { transform: 'scale(0.95)', opacity: 0 },
'100%': { transform: 'scale(1)', opacity: 1 },
},
dropdownFadeOut: {
'0%': { transform: 'scale(1)', opacity: 1 },
'100%': { transform: 'scale(0.95)', opacity: 0 },
},
fadeInOverlayBg: {
'0%': { opacity: 0 },
'100%': { opacity: 0.75 },
},
fadeOutOverlayBg: {
'0%': { opacity: 0.75 },
'100%': { opacity: 0 },
},
slideDown: {
'0%': { height: 0, opacity: 0 },
'100%': {
height: 'var(--radix-accordion-content-height)',
opacity: 1,
},
},
slideUp: {
'0%': { height: 'var(--radix-accordion-content-height)', opacity: 1 },
'100%': { height: 0, opacity: 0 },
},
slideDownNormal: {
'0%': { height: 0, opacity: 0 },
'100%': {
height: 'inherit',
opacity: 1,
},
},
slideUpNormal: {
'0%': { height: 'inherit', opacity: 1 },
'100%': { height: 0, opacity: 0 },
},
panelSlideLeftOut: {
'0%': { transform: 'translateX(-100%)', opacity: 0 },
'100%': {
transform: 'translate-x-0',
opacity: 1,
},
},
panelSlideLeftIn: {
'0%': { transform: 'translate-x-0', opacity: 1 },
'100%': { transform: 'translateX(-100%)', opacity: 0 },
},
panelSlideRightOut: {
'0%': { transform: 'translateX(100%)', opacity: 0 },
'100%': {
transform: 'translate-x-0',
opacity: 1,
},
},
panelSlideRightIn: {
'0%': { transform: 'translate-x-0', opacity: 1 },
'100%': { transform: 'translateX(100%)', opacity: 0 },
},
},
animation: {
'fade-in': 'fadeIn 300ms',
'fade-out': 'fadeOut 300ms',
'dropdown-content-show': 'overlayContentShow 100ms cubic-bezier(0.16, 1, 0.3, 1)',
'dropdown-content-hide': 'overlayContentHide 100ms cubic-bezier(0.16, 1, 0.3, 1)',
'overlay-show': 'overlayContentShow 300ms cubic-bezier(0.16, 1, 0.3, 1)',
'overlay-hide': 'overlayContentHide 300ms cubic-bezier(0.16, 1, 0.3, 1)',
'fade-in-overlay-bg': 'fadeInOverlayBg 300ms',
'fade-out-overlay-bg': 'fadeOutOverlayBg 300ms',
'slide-down': 'slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1)',
'slide-up': 'slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1)',
'slide-down-normal': 'slideDownNormal 300ms cubic-bezier(0.87, 0, 0.13, 1)',
'slide-up-normal': 'slideUpNormal 300ms cubic-bezier(0.87, 0, 0.13, 1)',
'panel-slide-left-out': 'panelSlideLeftOut 200ms cubic-bezier(0.87, 0, 0.13, 1)',
'panel-slide-left-in': 'panelSlideLeftIn 250ms cubic-bezier(0.87, 0, 0.13, 1)',
'panel-slide-right-out': 'panelSlideRightOut 200ms cubic-bezier(0.87, 0, 0.13, 1)',
'panel-slide-right-in': 'panelSlideRightIn 250ms cubic-bezier(0.87, 0, 0.13, 1)',
// tailwind class for this is `animate-dropdownFadeIn`
dropdownFadeIn: 'dropdownFadeIn 0.1s ease-out',
// tailwind class for this is `animate-dropdownFadeOut`
dropdownFadeOut: 'dropdownFadeOut 0.1s ease-out',
},
colors: {
...colorClasses,
'hi-contrast': `var(--colors-fixed-scale12)`,
'lo-contrast': `var(--colors-fixed-scale1)`,
},
},
},
plugins: [
require('@mertasan/tailwindcss-variables'),
function ({ addUtilities, addVariant }) {
// addVariant('data-open', '&:[data-state=open]')
addUtilities({
".dropdown-content[data-state='open']": {
animation: 'fadeIn 50ms ease-out',
},
".dropdown-content[data-state='closed']": {
animation: 'fadeOut 50ms ease-in',
},
"[data-state='open'] .accordion-content-animation": {
animation: 'slideDown 200ms ease-out',
},
"[data-state='closed'] .accordion-content-animation": {
animation: 'slideUp 200ms ease-in',
},
'.text-code': {
margin: '0 0.2em',
padding: '0.2em 0.4em 0.1em',
background: 'hsla(0, 0%, 58.8%, 0.1)',
border: '1px solid hsla(0, 0%, 39.2%, 0.2)',
borderRadius: '3px',
},
'.no-scrollbar': {
/* Hide scrollbar for IE, Edge*/
'-ms-overflow-style': 'none',
/* Firefox */
'scrollbar-width': 'none' /* Firefox */,
/* Hide scrollbar for Chrome, Safari and Opera */
'&::-webkit-scrollbar': {
display: 'none',
},
},
/* Add fadeout effect */
'.mask-fadeout-right': {
'-webkit-mask-image': 'linear-gradient(to right, white 98%, transparent 100%)',
'mask-image': 'linear-gradient(to right, white 98%, transparent 100%)',
},
'.mask-fadeout-left': {
'-webkit-mask-image': 'linear-gradient(to left, white 98%, transparent 100%)',
'mask-image': 'linear-gradient(to left, white 98%, transparent 100%)',
},
'input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button':
{
'-webkit-appearance': 'none',
margin: '0',
},
})
addVariant('data-open-parent', '[data-state="open"] &')
addVariant('data-closed-parent', '[data-state="closed"] &')
addVariant('data-open', '&[data-state="open"]')
addVariant('data-closed', '&[data-state="closed"]')
addVariant('data-show', '&[data-state="show"]')
addVariant('data-hide', '&[data-state="hide"]')
addVariant('data-checked', '&[data-state="checked"]')
addVariant('data-unchecked', '&[data-state="unchecked"]')
addVariant('aria-expanded', '&[aria-expanded="true"]')
// addVariant('parent-data-open', '[data-state="open"]&')
},
function ({ matchUtilities, theme }) {
matchUtilities(
{
highlight: (value) => ({ boxShadow: `inset 0 1px 0 0 ${value}` }),
},
{ values: flattenColorPalette(theme('backgroundColor')), type: 'color' }
),
matchUtilities(
{
subhighlight: (value) => ({
boxShadow: `inset 0 -1px 0 0 ${value}`,
}),
},
{
values: flattenColorPalette(theme('backgroundColor')),
type: 'color',
}
),
matchUtilities(
{
bordershadow: (value) => {
return {
boxShadow: `
var(--colors-blacka1) 0px 0px 0px 0px,
var(--colors-blacka1) 0px 0px 0px 0px,
var(--colors-blacka8) 0px 1px 1px 0px,
${value} 0px 0px 0px 1px,
var(--colors-blacka1) 0px 0px 0px 0px,
var(--colors-blacka1) 0px 0px 0px 0px,
rgb(64 68 82 / 8%) 0px 2px 5px 0px;
`,
}
},
},
{
values: flattenColorPalette(theme('backgroundColor')),
type: 'color',
}
)
},
require('tailwindcss-radix')(),
forms,
],
}
function arrayMergeFn(destinationArray, sourceArray) {
return destinationArray.concat(sourceArray).reduce((acc, cur) => {
if (acc.includes(cur)) return acc
return [...acc, cur]
}, [])
}
/**
* Merge Supabase UI and Tailwind CSS configurations
* @param {object} tailwindConfig - Tailwind config object
* @return {object} new config object
*/
function wrapper(tailwindConfig) {
let purge
if (Array.isArray(tailwindConfig.purge)) {
purge = {
content: tailwindConfig.purge,
}
} else {
purge = tailwindConfig.purge
}
return deepMerge({ ...tailwindConfig, purge }, uiConfig, {
arrayMerge: arrayMergeFn,
})
}
module.exports = wrapper
-1
View File
@@ -16,7 +16,6 @@ LOGFLARE_API_KEY=api-key
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_GOTRUE_URL=$SUPABASE_PUBLIC_URL/auth/v1
NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001
NEXT_PUBLIC_ENABLE_LOGS=true
# CmdK / AI
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzUwOTQ4MzUsImV4cCI6MTk5MDY3MDgzNX0.0PMlOxtKL4O9GGZuAP_Xl4f-Tut1qOnW4bNEmAtoB8w
@@ -1,4 +1,4 @@
import { Menu, Item, ItemParams, PredicateParams } from 'react-contexify'
import { Menu, Item, ItemParams, PredicateParams, Separator } from 'react-contexify'
import { IconTrash, IconClipboard, IconEdit } from 'ui'
import { useDispatch, useTrackedState } from '../../store'
import { formatClipboardValue, copyToClipboard } from '../../utils'
@@ -84,11 +84,7 @@ const RowContextMenu = ({ table, rows }: RowContextMenuProps) => {
return (
<>
<Menu
id={ROW_CONTEXT_MENU_ID}
animation={false}
className="!bg-scale-300 border border-scale-500"
>
<Menu id={ROW_CONTEXT_MENU_ID} animation={false}>
<Item onClick={onCopyCellContent}>
<IconClipboard size="tiny" />
<span className="ml-2 text-xs">Copy cell content</span>
@@ -97,6 +93,7 @@ const RowContextMenu = ({ table, rows }: RowContextMenuProps) => {
<IconEdit size="tiny" />
<span className="ml-2 text-xs">Edit row</span>
</Item>
<Separator />
<Item onClick={onDeleteRow} hidden={isItemHidden} data="delete">
<IconTrash size="tiny" stroke="red" />
<span className="ml-2 text-xs">Delete row</span>
@@ -14,7 +14,7 @@ const RedirectUrls = () => {
const { authConfig, ui } = useStore()
const URI_ALLOW_LIST_ARRAY = authConfig.config.URI_ALLOW_LIST
? authConfig.config.URI_ALLOW_LIST.split(',')
? authConfig.config.URI_ALLOW_LIST.split(/\s*[,]+\s*/).filter((url: string) => url)
: []
const [open, setOpen] = useState(false)
@@ -28,9 +28,14 @@ const RedirectUrls = () => {
})
const onAddNewUrl = async (values: any, { setSubmitting }: any) => {
if (!values.url) {
return
}
setSubmitting(true)
const payload = URI_ALLOW_LIST_ARRAY
payload.push(values.url)
// remove any trailing commas
payload.push(values.url.replace(/,\s*$/, ''))
const payloadString = payload.toString()
@@ -24,6 +24,24 @@ const UtilityTabResults = ({ id, isExecuting }: UtilityTabResultsProps) => {
return (
<div className="bg-table-header-light dark:bg-table-header-dark">
<p className="m-0 border-0 px-6 py-4 font-mono">{result.error.message ?? result.error}</p>
{(result.error.message ?? result.error)?.includes(
'canceling statement due to statement timeout'
) && (
<p className="m-0 border-0 px-6 py-4 font-mono">
You can either{' '}
<a
className="underline"
href="https://supabase.com/docs/guides/platform/performance#examining-query-performance"
>
optimize your query
</a>
, or{' '}
<a className="underline" href="https://supabase.com/docs/guides/database/timeouts">
increase the statement timeout
</a>
.
</p>
)}
</div>
)
} else if (!result) {
@@ -667,4 +667,4 @@ export const TIER_QUERY_LIMITS: {
ENTERPRISE: { text: '90 days', value: 90, unit: 'day', promptUpgrade: false },
}
export const SHOW_O11Y = IS_PLATFORM ? true : process.env.NEXT_PUBLIC_ENABLE_LOGS == 'true'
export const SHOW_O11Y = IS_PLATFORM || process.env.NEXT_PUBLIC_ENABLE_LOGS == 'true'
@@ -131,7 +131,7 @@ const LogsQueryPanel = ({
<Button type="default" onClick={onClear}>
Clear query
</Button>
{IS_PLATFORM && onSave && (
{IS_PLATFORM && onSave && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
@@ -214,7 +214,7 @@ const Column: FC<Props> = ({
</>
)}
{isNewRecord && (
{column.isNewColumn && (
<>
<Checkbox
label="Is Unique"
@@ -97,7 +97,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
<div className="ml-2">
<Link href={`/project/${projectRef}/settings/billing/usage`}>
<a>
<Badge color="red">Project has exceeded usage limits </Badge>
<Badge color="red">Exceeding usage limits</Badge>
</a>
</Link>
</div>
@@ -1,44 +0,0 @@
import React, { useState } from 'react'
import { Modal } from 'ui'
export default function ConfirmModal({
title,
message,
onConfirm,
onClose,
onAsyncConfirm,
visible,
}) {
const [loading, setLoading] = useState(false)
function onCancelClick() {
if (!loading) onClose()
}
async function onConfirmClick() {
if (onAsyncConfirm) {
setLoading(true)
await onAsyncConfirm()
onClose()
} else if (onConfirm) {
onConfirm()
}
}
return (
<Modal
variant="warning"
visible={visible}
title={title}
description={message}
showIcon={false}
size="small"
onConfirmText="OK"
onCancelText="Cancel"
onCancel={onCancelClick}
onConfirm={onConfirmClick}
loading={loading}
/>
)
}
@@ -1,73 +0,0 @@
import { Transition } from '@headlessui/react'
import { clickOutsideListener, useKeyboardShortcuts } from 'hooks'
/**
* Base modal with trigger button and close button
*
* @param {Boolean} open // The parent should hold the open state
* @param {Function} handleCloseEvent // Delegate the close event to the parent
* @param {JSX.Element|JSX.Element[]} children
*/
export default function Modal({ children, open, handleCloseEvent }) {
const ref = clickOutsideListener(handleCloseEvent)
useKeyboardShortcuts({
Escape: handleCloseEvent,
})
return (
<div className="relative">
<Transition
show={open}
enter="ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed z-50 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity">
<div className="absolute inset-0 bg-black opacity-75"></div>
</div>
<div className="absolute top-0 right-0 pt-4 pr-4">
<button type="button" aria-label="Close">
<svg
className="h-6 w-6 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div
ref={ref}
className="flex justify-center items-center w-full h-screen"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
style={{
maxHeight: '100vh',
overflow: 'auto',
}}
>
<div className="text-left rounded m-auto shadow-xl transform transition-all bg-gray-700 w-[28rem]">
{children}
</div>
</div>
</div>
</div>
</Transition>
</div>
)
}
@@ -10,7 +10,14 @@ import {
STORAGE_ROW_TYPES,
} from '../Storage.constants'
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
import { IconFolderPlus } from 'ui'
import {
IconEye,
IconChevronRight,
IconFolderPlus,
IconClipboard,
IconChevronsUp,
IconChevronsDown,
} from 'ui'
interface Props {
id: string
@@ -57,21 +64,24 @@ const ColumnContextMenu: FC<Props> = ({ id = '' }) => {
}
return (
<Menu id={id} animation="fade" className="!bg-scale-300 border border-scale-500">
<Menu id={id} animation="fade">
<Item onClick={({ props }) => onSelectCreateFolder(props.index)}>
<IconFolderPlus size="tiny" />
<span className="ml-2 text-xs">New folder</span>
</Item>
<Separator />
<Item onClick={({ props }) => onSelectAllItemsInColumn(props.index)}>
<IconClipboard size="tiny" />
<span className="ml-2 text-xs">Select all items</span>
</Item>
<Submenu
label={
<div>
<span className="ml-2 text-xs">View</span>
<div className="flex items-center space-x-2">
<IconEye size="tiny" />
<span className="text-xs">View</span>
</div>
}
arrow={<IconChevronRight size="tiny" />}
>
<Item onClick={() => setView(STORAGE_VIEWS.COLUMNS)}>
<span className="ml-2 text-xs">As columns</span>
@@ -82,10 +92,12 @@ const ColumnContextMenu: FC<Props> = ({ id = '' }) => {
</Submenu>
<Submenu
label={
<div>
<div className="flex items-center space-x-2">
<IconChevronsDown size="tiny" />
<span className="ml-2 text-xs">Sort by</span>
</div>
}
arrow={<IconChevronRight size="tiny" />}
>
<Item onClick={() => setSortBy(STORAGE_SORT_BY.NAME)}>
<span className="ml-2 text-xs">Name</span>
@@ -102,10 +114,12 @@ const ColumnContextMenu: FC<Props> = ({ id = '' }) => {
</Submenu>
<Submenu
label={
<div>
<div className="flex items-center space-x-2">
<IconChevronsUp size="tiny" />
<span className="ml-2 text-xs">Sort by order</span>
</div>
}
arrow={<IconChevronRight size="tiny" />}
>
<Item onClick={() => setSortByOrder(STORAGE_SORT_BY_ORDER.ASC)}>
<span className="ml-2 text-xs">Ascending</span>
@@ -17,7 +17,7 @@ const FolderContextMenu: FC<Props> = ({ id = '' }) => {
const canUpdateFiles = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*')
return (
<Menu id={id} animation="fade" className="!bg-scale-300 border border-scale-500">
<Menu id={id} animation="fade">
{canUpdateFiles && (
<Item onClick={({ props }) => setSelectedItemToRename(props.item)}>
<IconEdit size="tiny" />
@@ -31,7 +31,7 @@ const FolderContextMenu: FC<Props> = ({ id = '' }) => {
{canUpdateFiles && [
<Separator />,
<Item onClick={({ props }) => setSelectedItemsToDelete([props.item])}>
<IconTrash2 size="tiny" />
<IconTrash2 size="tiny" stroke="red" />
<span className="ml-2 text-xs">Delete</span>
</Item>,
]}
@@ -45,7 +45,7 @@ const ItemContextMenu: FC<Props> = ({ id = '' }) => {
}
return (
<Menu id={id} animation="fade" className="!bg-scale-300 border border-scale-500">
<Menu id={id} animation="fade">
{isPublic ? (
<Item onClick={({ props }) => onHandleClick('copy', props.item)}>
<IconClipboard size="tiny" />
@@ -96,7 +96,7 @@ const ItemContextMenu: FC<Props> = ({ id = '' }) => {
</Item>,
<Separator key="file-separator" />,
<Item key="delete-file" onClick={({ props }) => setSelectedItemsToDelete([props.item])}>
<IconTrash2 size="tiny" />
<IconTrash2 size="tiny" stroke="red" />
<span className="ml-2 text-xs">Delete</span>
</Item>,
]}
+12 -13
View File
@@ -10,8 +10,9 @@ const PageTelemetry: FC = ({ children }) => {
const { ui } = useStore()
useEffect(() => {
function handleRouteChange(url: string) {
handlePageTelemetry(url)
function handleRouteChange() {
// We want to send dynamic route path
handlePageTelemetry(router.route)
}
// Listen for page changes after a navigation or when the query changes
@@ -19,18 +20,19 @@ const PageTelemetry: FC = ({ children }) => {
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])
}, [router])
useEffect(() => {
/**
* Send page telemetry on first page load
* if the route is not ready. Don't need to send it will be picked up by router.event above
*/
if (router.isReady) {
handlePageTelemetry(router.asPath)
}
// Send page telemetry on first page load
// We want to send dynamic route path
handlePageTelemetry(router.route)
}, [])
/**
* send page_view event
*
* @param route: dynamic route path. Don't use the browser url
* */
const handlePageTelemetry = async (route?: string) => {
if (IS_PLATFORM) {
/**
@@ -40,9 +42,6 @@ const PageTelemetry: FC = ({ children }) => {
/**
* Send page telemetry
*
* TODO: document.title is lagging behind routeChangeComplete
* that means the page title is the previous one instead of the new page title
*/
post(`${API_URL}/telemetry/page`, {
referrer: referrer,
+1 -1
View File
@@ -181,7 +181,7 @@ const JoinOrganizationPage = () => {
</div>
<div
className={['border-t border-scale-400', isError ? 'bg-sand-100' : 'bg-transparent'].join(
className={['border-t border-scale-400', isError ? 'bg-scale-100' : 'bg-transparent'].join(
' '
)}
>
@@ -24,6 +24,12 @@ const PROVIDER_EMAIL = {
If disabled, only the new email is required to confirm.`,
type: 'boolean',
},
SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION: {
title: 'Secure password change',
description: `Users will need to be recently logged in to change their password.
If disabled, a user can change their password at any time.`,
type: 'boolean',
},
MAILER_OTP_EXP: {
title: 'Mailer OTP Expiration',
type: 'number',
+11 -9
View File
@@ -288,8 +288,8 @@ export default class MetaStore implements IMetaStore {
}
async addPrimaryKey(schema: string, table: string, columns: string[]) {
const primaryKeyColumns = columns.join(',')
const query = `ALTER TABLE "${schema}"."${table}" ADD PRIMARY KEY (${primaryKeyColumns})`
const primaryKeyColumns = columns.join('","')
const query = `ALTER TABLE "${schema}"."${table}" ADD PRIMARY KEY ("${primaryKeyColumns}")`
return await this.query(query)
}
@@ -498,11 +498,15 @@ export default class MetaStore implements IMetaStore {
const removePK = await this.removePrimaryKey(column.schema, column.table)
if (removePK.error) throw removePK.error
}
const primaryKeyColumns = isPrimaryKey
? existingPrimaryKeys.concat([column.name])
: existingPrimaryKeys.filter((x) => x !== column.name)
const addPK = await this.addPrimaryKey(column.schema, column.table, primaryKeyColumns)
if (addPK.error) throw addPK.error
if (primaryKeyColumns.length) {
const addPK = await this.addPrimaryKey(column.schema, column.table, primaryKeyColumns)
if (addPK.error) throw addPK.error
}
}
// For updating of foreign key relationship, we remove the original one by default
@@ -647,7 +651,7 @@ export default class MetaStore implements IMetaStore {
// Then add the primary key constraints here to support composite keys
const primaryKeyColumns = columns
.filter((column: ColumnField) => column.isPrimaryKey)
.map((column: ColumnField) => `"${column.name}"`)
.map((column: ColumnField) => column.name)
if (primaryKeyColumns.length > 0) {
const primaryKeys = await this.addPrimaryKey(table.schema, table.name, primaryKeyColumns)
if (primaryKeys.error) throw primaryKeys.error
@@ -747,11 +751,9 @@ export default class MetaStore implements IMetaStore {
// Prepare a check to see if primary keys to the tables were updated or not
const primaryKeyColumns = columns
.filter((column) => column.isPrimaryKey)
.map((column) => `"${column.name}"`)
.map((column) => column.name)
// @ts-ignore
const existingPrimaryKeyColumns = table.primary_keys.map(
(pk: PostgresPrimaryKey) => `"${pk.name}"`
)
const existingPrimaryKeyColumns = table.primary_keys.map((pk: PostgresPrimaryKey) => pk.name)
const isPrimaryKeyUpdated = !isEqual(primaryKeyColumns, existingPrimaryKeyColumns)
if (isPrimaryKeyUpdated) {
+1 -1
View File
@@ -1,5 +1,5 @@
.react-contexify {
@apply dark:bg-gray-700;
@apply dark:bg-scale-300 border border-scale-500;
.react-contexify__item {
.react-contexify__item__content {
@apply dark:text-white text-sm;