Merge pull request #9635 from supabase/master

Prod deploy for Dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2022-10-18 13:39:39 +08:00
commit 859327c816
32 files changed
+678 -420

No files matched your search

+12 -13
View File
@@ -9,7 +9,6 @@ on:
- 'studio/**'
jobs:
build:
runs-on: ubuntu-latest
strategy:
@@ -18,15 +17,15 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install deps
run: npm i
working-directory: ./studio
- name: Run build
run: npm run build
working-directory: ./studio
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install deps
run: npm ci
working-directory: ./
- name: Run build
run: npx turbo run build --filter=studio
working-directory: ./
+1 -1
View File
@@ -14,7 +14,7 @@ const config = {
url: 'https://supabase.com',
baseUrl: baseUrl,
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
onBrokenMarkdownLinks: 'throw',
favicon: '/favicon.ico',
themes: ['docusaurus-theme-search-typesense'],
+88 -74
View File
@@ -270,7 +270,7 @@ module.exports = [
{
permanent: false,
source: '/docs/client/auth-session',
destination: '/docs/reference/javascript/auth-session',
destination: '/docs/reference/javascript/v1/auth-session',
},
{
permanent: false,
@@ -325,12 +325,12 @@ module.exports = [
{
permanent: false,
source: '/docs/client/removesubscription',
destination: '/docs/reference/javascript/removesubscription',
destination: '/docs/reference/javascript/v1/removesubscription',
},
{
permanent: false,
source: '/docs/client/getsubscriptions',
destination: '/docs/reference/javascript/getsubscriptions',
destination: '/docs/reference/javascript/v1/getsubscriptions',
},
{
permanent: false,
@@ -1359,65 +1359,84 @@ module.exports = [
destination: '/docs/guides/auth/auth-helpers/sveltekit',
},
// V2 redirects
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-update',
// destination: '/docs/reference/javascript/auth-updateuser',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-getuser',
// destination: '/docs/reference/javascript/auth-getuser',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-resetpasswordforemail',
// destination: '/docs/reference/javascript/auth-resetpasswordforemail',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-verifyotp',
// destination: '/docs/reference/javascript/auth-verifyotp',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-listusers',
// destination: '/docs/reference/javascript/auth-admin-listusers',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-createuser',
// destination: '/docs/reference/javascript/auth-admin-createuser',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-deleteuser',
// destination: '/docs/reference/javascript/auth-admin-deleteuser',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-generatelink',
// destination: '/docs/reference/javascript/auth-admin-generatelink',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-inviteuserbyemail',
// destination: '/docs/reference/javascript/auth-admin-inviteuserbyemail',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-getuserbyid',
// destination: '/docs/reference/javascript/auth-admin-getuserbyid',
// },
// {
// permanent: true,
// source: '/docs/reference/javascript/auth-api-updateuserbyid',
// destination: '/docs/reference/javascript/auth-admin-updateuserbyid',
// },
// v1 to v2 redirects
// supabase-js v1 to v2 redirects
// v1: /auth-update
// v2: /auth-updateuser
{
permanent: true,
source: '/docs/reference/javascript/auth-update',
destination: '/docs/reference/javascript/v1/auth-update',
},
// v1: /auth-api-get-user
// v2: /auth-getuser
{
permanent: true,
source: '/docs/reference/javascript/auth-api-getuser',
destination: '/docs/reference/javascript/v1/auth-api-getuser',
},
// v1: /auth-api-resetpasswordforemail
// v2: /auth-resetpasswordforemail
{
permanent: true,
source: '/docs/reference/javascript/auth-api-resetpasswordforemail',
destination: '/docs/reference/javascript/v1/auth-api-resetpasswordforemail',
},
// v1: /auth-api-verifyotp
// v2: /auth-verifyotp
{
permanent: true,
source: '/docs/reference/javascript/auth-api-verifyotp',
destination: '/docs/reference/javascript/v1/auth-api-verifyotp',
},
// v1: /auth-api-listusers
// v2: /auth-admin-listusers
{
permanent: true,
source: '/docs/reference/javascript/auth-api-listusers',
destination: '/docs/reference/javascript/v1/auth-api-listusers',
},
// v1: /auth-api-createuser
// v2: /auth-admin-createuser
{
permanent: true,
source: '/docs/reference/javascript/auth-api-createuser',
destination: '/docs/reference/javascript/v1/auth-api-createuser',
},
// v1: /auth-api-deleteuser
// v2: /auth-admin-deleteuser
{
permanent: true,
source: '/docs/reference/javascript/auth-api-deleteuser',
destination: '/docs/reference/javascript/v1/auth-api-deleteuser',
},
// v1: /auth-api-generatelink
// v2: /auth-admin-generatelink
{
permanent: true,
source: '/docs/reference/javascript/auth-api-generatelink',
destination: '/docs/reference/javascript/v1/auth-api-generatelink',
},
// v1: /auth-api-inviteuserbyemail
// v2: /auth-admin-inviteuserbyemail
{
permanent: true,
source: '/docs/reference/javascript/auth-api-inviteuserbyemail',
destination: '/docs/reference/javascript/v1/auth-api-inviteuserbyemail',
},
// v1: /auth-api-getuserbyid
// v2: /auth-admin-getuserbyid
{
permanent: true,
source: '/docs/reference/javascript/auth-api-getuserbyid',
destination: '/docs/reference/javascript/v1/auth-api-getuserbyid',
},
// v1: /auth-api-updateuserbyid
// v2: /auth-admin-updateuserbyid
{
permanent: true,
source: '/docs/reference/javascript/auth-api-updateuserbyid',
destination: '/docs/reference/javascript/v1/auth-api-updateuserbyid',
},
// signIn method is now split into signInWithPassword ,signInWithPasswordless ,signInWithOAuth
// send traffic to v1 docs instead
{
@@ -1425,26 +1444,21 @@ module.exports = [
source: '/docs/reference/javascript/auth-signin',
destination: '/docs/reference/javascript/v1/auth-signin',
},
// auth.session renamed to auth.getSession
// send to new v2 docs version
// v1: /auth-session
// v2: /auth-getsession
{
permanent: true,
source: '/docs/reference/javascript/auth-session',
destination: '/docs/reference/javascript/auth-getsession',
destination: '/docs/reference/javascript/v1/auth-session',
},
// Removed sendMobileOTP in favor of signInWithOtp
// send traffic to new v2 docs instead
// v1: /auth-api-sendmobileotp
// v2: /auth-signinwithotp
{
permanent: true,
source: '/docs/reference/javascript/auth-api-sendmobileotp',
destination: '/docs/reference/javascript/auth-signinwithotp',
},
// Replaced update with updateUser
{
permanent: true,
source: '/docs/reference/javascript/auth-update',
destination: '/docs/reference/javascript/auth-updateuser',
destination: '/docs/reference/javascript/v1/auth-api-sendmobileotp',
},
// realtime methods been replaced with new names
{
permanent: true,
+2 -3
View File
@@ -52,7 +52,7 @@ export const Section = ({ children, icon, img }) => (
### SOC 2
Supabase is SOC2 Type 1 compliant. Enterprise customers can request a copy of our SOC2 through our [Security Portal](https://security.supabase.com).
Supabase is SOC2 Type 1 compliant. Enterprise customers can request a copy of our SOC2 [here](https://supabase.com/legal/soc2).
<div className="w-16">
<img src="/images/security/21972-312_SOC_NonCPA.png" />
@@ -126,8 +126,7 @@ In addition to internal security reviews, we use various tools to scan our code
<h3 className="text-3xl">Security Portal</h3>
Access Supabase’s security and compliance documents on our Security Portal, such as
penetration testing and audit reports.
Access more of Supabase’s security on our Security Portal, such as penetration testing and audit reports.
<Link passHref href="https://security.supabase.com">
<Button type="default" size="medium" as="a" className="no-underline" target="_blank">
+13
View File
@@ -16,6 +16,7 @@
],
"dependencies": {
"@mdx-js/react": "^2.1.1",
"html-to-image": "^1.10.1",
"mini-svg-data-uri": "^1.4.4"
},
"devDependencies": {
@@ -18169,6 +18170,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/html-to-image": {
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.10.8.tgz",
"integrity": "sha512-t+JyFJwKDCp4ZwBp4iC/wqw0meQDDc77Qs8OFl5P7RGlIP3LQMvwpD7VXxqQfC7/TfC+GKYlFP6WDYfXTmXHfw=="
},
"node_modules/html-void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
@@ -36098,6 +36104,7 @@
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"json-logic-js": "^2.0.2",
"lodash": "^4.17.21",
@@ -50190,6 +50197,11 @@
"integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==",
"dev": true
},
"html-to-image": {
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.10.8.tgz",
"integrity": "sha512-t+JyFJwKDCp4ZwBp4iC/wqw0meQDDc77Qs8OFl5P7RGlIP3LQMvwpD7VXxqQfC7/TfC+GKYlFP6WDYfXTmXHfw=="
},
"html-void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
@@ -60621,6 +60633,7 @@
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"jest": "^27.4.0",
"jest-canvas-mock": "^2.3.1",
+172 -8
View File
@@ -1,7 +1,7 @@
clispec: '001'
info:
id: cli
version: 1.0.0
version: 1.8.5
title: Supabase CLI
language: sh
source: https://github.com/supabase/cli
@@ -11,8 +11,9 @@ info:
Supabase CLI provides you with tools to develop your application locally, and deploy your application to the Supabase platform.
options: |-
```sh
--debug output debug logs to stderr
-h, --help help for supabase
--debug output debug logs to stderr
-h, --help help for supabase
--workdir string path to a Supabase project directory
```
commands:
- id: supabase-stop
@@ -27,7 +28,8 @@ commands:
subcommands: []
options: |-
```sh
-h, --help help for stop
--backup Backs up the current database before stopping.
-h, --help help for stop
```
- id: supabase-status
title: supabase status
@@ -299,7 +301,7 @@ commands:
title: supabase gen types typescript
summary: Generate types for TypeScript
description: |
Generate types for TypeScript. Must specify either --local or --db-url
Generate types for TypeScript. Must specify one of --local, --linked, --project-id, or --db-url
tags: []
links: []
usage: |-
@@ -309,9 +311,12 @@ commands:
subcommands: []
options: |-
```sh
--db-url string Generate types from a database url.
-h, --help help for typescript
--local Generate types from the local dev database.
--db-url string Generate types from a database url.
-h, --help help for typescript
--linked Generate types from the linked project.
--local Generate types from the local dev database.
--project-id string Generate types from a project ID.
--schema stringArray Schemas to generate types for.
```
- id: supabase-functions
title: supabase functions
@@ -399,6 +404,7 @@ commands:
subcommands:
- supabase-db-branch
- supabase-db-diff
- supabase-db-lint
- supabase-db-push
- supabase-db-remote
- supabase-db-reset
@@ -478,6 +484,22 @@ commands:
-h, --help help for push
-p, --password string Password to your remote Postgres database.
```
- id: supabase-db-lint
title: supabase db lint
summary: Checks local database for typing error
tags: []
links: []
usage: |-
```sh
supabase db lint [flags]
```
subcommands: []
options: |-
```sh
-h, --help help for lint
--level string Error level to emit. (default "warning")
-s, --schema strings List of schema to include. (default [public])
```
- id: supabase-db-diff
title: supabase db diff
summary: Diffs the local database for schema changes
@@ -567,3 +589,145 @@ commands:
```sh
-h, --help help for create
```
- id: supabase-completion
title: supabase completion
summary: Generate the autocompletion script for the specified shell
description: |
Generate the autocompletion script for supabase for the specified shell.
See each sub-command's help for details on how to use the generated script.
tags: []
links: []
subcommands:
- supabase-completion-bash
- supabase-completion-fish
- supabase-completion-powershell
- supabase-completion-zsh
options: |-
```sh
-h, --help help for completion
```
- id: supabase-completion-zsh
title: supabase completion zsh
summary: Generate the autocompletion script for zsh
description: |
Generate the autocompletion script for the zsh shell.
If shell completion is not already enabled in your environment you will need
to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions in your current shell session:
source <(supabase completion zsh); compdef _supabase supabase
To load completions for every new session, execute once:
#### Linux:
supabase completion zsh > "${fpath[1]}/_supabase"
#### macOS:
supabase completion zsh > $(brew --prefix)/share/zsh/site-functions/_supabase
You will need to start a new shell for this setup to take effect.
tags: []
links: []
usage: |-
```sh
supabase completion zsh [flags]
```
subcommands: []
options: |-
```sh
-h, --help help for zsh
--no-descriptions disable completion descriptions
```
- id: supabase-completion-powershell
title: supabase completion powershell
summary: Generate the autocompletion script for powershell
description: |
Generate the autocompletion script for powershell.
To load completions in your current shell session:
supabase completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
tags: []
links: []
usage: |-
```sh
supabase completion powershell [flags]
```
subcommands: []
options: |-
```sh
-h, --help help for powershell
--no-descriptions disable completion descriptions
```
- id: supabase-completion-fish
title: supabase completion fish
summary: Generate the autocompletion script for fish
description: |
Generate the autocompletion script for the fish shell.
To load completions in your current shell session:
supabase completion fish | source
To load completions for every new session, execute once:
supabase completion fish > ~/.config/fish/completions/supabase.fish
You will need to start a new shell for this setup to take effect.
tags: []
links: []
usage: |-
```sh
supabase completion fish [flags]
```
subcommands: []
options: |-
```sh
-h, --help help for fish
--no-descriptions disable completion descriptions
```
- id: supabase-completion-bash
title: supabase completion bash
summary: Generate the autocompletion script for bash
description: |
Generate the autocompletion script for the bash shell.
This script depends on the 'bash-completion' package.
If it is not installed already, you can install it via your OS's package manager.
To load completions in your current shell session:
source <(supabase completion bash)
To load completions for every new session, execute once:
#### Linux:
supabase completion bash > /etc/bash_completion.d/supabase
#### macOS:
supabase completion bash > $(brew --prefix)/etc/bash_completion.d/supabase
You will need to start a new shell for this setup to take effect.
tags: []
links: []
usage: |-
```sh
supabase completion bash
```
subcommands: []
options: |-
```sh
-h, --help help for bash
--no-descriptions disable completion descriptions
```
@@ -53,7 +53,7 @@ const Users = () => {
}
return (
<div className="overflow-scroll">
<div>
<div className="justify-between px-6 pt-6 pb-2 md:flex">
<div className="relative flex space-x-4">
<Input
@@ -108,8 +108,8 @@ const Users = () => {
{inviteEnabled && <InviteUserModal />}
</div>
</div>
<section className="mt-4 overflow-visible px-6">
<div className="section-block--body relative overflow-scroll rounded">
<section className="users-table-container mt-4 overflow-visible px-6">
<div className="section-block--body relative overflow-x-scroll rounded">
<div className="inline-block min-w-full align-middle">
<UsersList />
</div>
@@ -1,16 +0,0 @@
export const CANCELLATION_REASONS = [
'Pricing',
"My project isn't getting traction",
'Poor customer service',
'Missing feature',
"I didn't see the value",
"Supabase didn't meet my needs",
'Dashboard is too complicated',
'Postgres is too complicated',
'Problem not solved',
'Bug issues',
'I decided to use something else',
'My work has finished/discontinued',
'I’m migrating to/starting a new project',
'None of the above',
]
@@ -0,0 +1,74 @@
import { IconArchive, IconDatabase, IconKey } from 'ui'
export const CANCELLATION_REASONS = [
'Pricing',
"My project isn't getting traction",
'Poor customer service',
'Missing feature',
"I didn't see the value",
"Supabase didn't meet my needs",
'Dashboard is too complicated',
'Postgres is too complicated',
'Problem not solved',
'Bug issues',
'I decided to use something else',
'My work has finished/discontinued',
'I’m migrating to/starting a new project',
'None of the above',
]
export const USAGE_BASED_PRODUCTS = [
{
title: 'Database',
icon: <IconDatabase className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{
key: 'db_size',
attribute: 'total_db_size_bytes',
title: 'Database space',
units: 'bytes',
costPerUnit: 0.125,
},
{
key: 'db_egress',
attribute: 'total_egress_modified',
title: 'Database egress',
units: 'bytes',
costPerUnit: 0.09,
},
],
},
{
title: 'Auth',
icon: <IconKey className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{
key: 'monthly_active_users',
attribute: 'total_auth_billing_period_mau',
title: 'Monthly Active Users',
units: 'absolute',
costPerUnit: 0.00325,
},
],
},
{
title: 'Storage',
icon: <IconArchive className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{
key: 'storage_size',
attribute: 'total_storage_size_bytes',
title: 'Storage space',
units: 'bytes',
costPerUnit: 0.021,
},
{
key: 'storage_egress',
attribute: 'total_storage_egress',
title: 'Storage egress',
units: 'bytes',
costPerUnit: 0.09,
},
],
},
]
@@ -1,48 +0,0 @@
import { ChargeableProduct } from './PAYGUsage.types'
export const chargeableProducts: ChargeableProduct[] = [
{
title: 'Database',
iconUrl: '/img/database.svg',
features: [
{
title: 'Database Size',
attribute: 'total_db_size_bytes',
costPerUnit: 0.125,
unitQuantity: 1000000000, // 1GB
pricingModel: 'max',
freeQuota: 8000000000, // 8GB
},
{
title: 'Database Egress',
attribute: 'total_egress_modified',
costPerUnit: 0.09,
unitQuantity: 1000000000, // 1GB
pricingModel: 'sum',
freeQuota: 50000000000, // 50GB
},
],
},
{
title: 'Storage',
iconUrl: '/img/archive.svg',
features: [
{
title: 'Storage Size',
attribute: 'total_storage_size_bytes',
costPerUnit: 0.021,
unitQuantity: 1000000000, // 1GB
pricingModel: 'max',
freeQuota: 100000000000, // 100GB
},
{
title: 'Storage Egress',
attribute: 'total_storage_egress',
costPerUnit: 0.09,
unitQuantity: 1000000000, // 1GB
pricingModel: 'sum',
freeQuota: 200000000000, // 200GB
},
],
},
]
@@ -1,14 +1,11 @@
import React, { FC } from 'react'
import { FC } from 'react'
import dayjs from 'dayjs'
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
import { ChargeableProduct, PaygStats, ProductFeature } from './PAYGUsage.types'
import { deriveProductCost } from './PAYGUsage.utils'
import { chargeableProducts } from './PAYGUsage.constants'
import { DATE_FORMAT } from 'lib/constants'
import { USAGE_BASED_PRODUCTS } from '../Billing.constants'
interface Props {
paygStats: PaygStats
dateRange: {
period_start: { date: string; time_period: string }
period_end: { date: string; time_period: string }
@@ -16,16 +13,14 @@ interface Props {
}
}
const PAYGUsage: FC<Props> = ({ paygStats, dateRange }) => {
const PAYGUsage: FC<Props> = ({ dateRange }) => {
const startDate =
dateRange.period_start.date ?? dayjs().utc().startOf('month').format(DATE_FORMAT)
const endDate = dateRange.period_end.date ?? dayjs().utc().endOf('month').format(DATE_FORMAT)
return (
<div className="flex flex-col">
{chargeableProducts.map((product: ChargeableProduct) => {
const productCost = deriveProductCost(paygStats, product)
{USAGE_BASED_PRODUCTS.map((product) => {
return (
<div
key={product.title}
@@ -36,21 +31,15 @@ const PAYGUsage: FC<Props> = ({ paygStats, dateRange }) => {
<div className="flex items-center justify-between overflow-hidden rounded px-6 py-3">
<div className="flex items-center space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded bg-white">
<img width={'16'} src={product.iconUrl} />
{product.icon}
</div>
<h5 className="mb-0 font-medium">{product.title}</h5>
</div>
{product.title !== 'Authentication' && (
<div className="flex items-center space-x-1">
<p className="text-scale-1000">$</p>
<h4 className="text-lg m-0 font-medium">{productCost.toFixed(3)}</h4>
</div>
)}
</div>
</div>
<div className="flex flex-col">
{product.features.map((feature: ProductFeature) => (
{product.features.map((feature) => (
<div
key={feature.title}
className="border-panel-border-light dark:border-panel-border-dark relative border-t px-6 py-3"
@@ -1,18 +0,0 @@
export interface ChargeableProduct {
title: string
iconUrl: string
features: ProductFeature[]
}
export interface ProductFeature {
title: string
attribute: string
costPerUnit: number
unitQuantity: number
pricingModel: PricingModel
freeQuota?: number
}
export type PaygStats = { [key: string]: { sum: number; max: number } }
export type PricingModel = 'sum' | 'max'
@@ -1,39 +0,0 @@
import { get, maxBy, sum } from 'lodash'
import { ChargeableProduct, PaygStats, ProductFeature } from './PAYGUsage.types'
/**
*
* Derives the most appropriate cost
* depending on what kind of product feature we are displaying cost for.
*
* @param paygStats
* @param feature
*
* @returns number
*/
export const deriveFeatureCost = (paygStats: PaygStats | undefined, feature: ProductFeature) => {
let rawUsage = paygStats?.[feature.attribute]?.[feature.pricingModel]
/**
* Some features have a free quota
*
* Check there is included free quota,
* and remove free quota amount from the raw usage derived from paygStats
*
* if the number is negative, then Math.max should return 0
*
* todo: move this logic to backend @mildtomato
*/
if (rawUsage && feature.freeQuota) {
rawUsage = Math.max(0, rawUsage - feature.freeQuota)
}
const maximumValueOfTheMonth = rawUsage ?? 0
return (maximumValueOfTheMonth / feature.unitQuantity) * feature.costPerUnit
}
export const deriveProductCost = (paygStats: any, product: ChargeableProduct) => {
const costs = product.features.map((feature) => deriveFeatureCost(paygStats, feature))
return sum(costs)
}
@@ -6,17 +6,17 @@ interface Props {
amount: number | string
unitPrice: number | string
price: number | string
freeQuota?: number
note?: string
}
const CostBreakdownRow: FC<Props> = ({ item, amount, unitPrice, price, freeQuota }) => {
const CostBreakdownRow: FC<Props> = ({ item, amount, unitPrice, price, note }) => {
return (
<div className="relative flex items-center border-t border-panel-border-light px-6 py-3 dark:border-panel-border-dark">
<div className="flex w-[40%] items-center gap-3">
<span className="text-sm">{item}</span>
{freeQuota && (
{note && (
<Badge color="scale">
<span className="text-xs text-scale-900">{`${freeQuota / 1000000000}GB included`}</span>
<span className="text-xs text-scale-900">{note}</span>
</Badge>
)}
</div>
@@ -1,27 +1,23 @@
import dayjs from 'dayjs'
import { FC } from 'react'
import { sum } from 'lodash'
import { useRouter } from 'next/router'
import { Button, Loading } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useStore, useFlag } from 'hooks'
import { checkPermissions, useStore, useFlag, useProjectUsage } from 'hooks'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { chargeableProducts } from '../PAYGUsage/PAYGUsage.constants'
import { PaygStats, ProductFeature } from '../PAYGUsage/PAYGUsage.types'
import { deriveFeatureCost, deriveProductCost } from '../PAYGUsage/PAYGUsage.utils'
import CostBreakdownRow from './CostBreakdownRow'
import { StripeSubscription } from './Subscription.types'
import NoPermission from 'components/ui/NoPermission'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
interface Props {
project: any
subscription: StripeSubscription
paygStats: PaygStats | undefined
loading?: boolean
showProjectName?: boolean
currentPeriodStart: number
@@ -31,7 +27,6 @@ interface Props {
const Subscription: FC<Props> = ({
project,
subscription,
paygStats,
loading = false,
showProjectName = false,
currentPeriodStart,
@@ -40,6 +35,7 @@ const Subscription: FC<Props> = ({
const { ui } = useStore()
const router = useRouter()
const { ref } = router.query
const enablePermissions = useFlag('enablePermissions')
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
@@ -48,6 +44,8 @@ const Subscription: FC<Props> = ({
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.subscriptions')
: ui.selectedOrganization?.is_owner
const { usage, isLoading: loadingUsage } = useProjectUsage(ref as string)
const isPayg = subscription?.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG
const isEnterprise = subscription.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
@@ -56,15 +54,23 @@ const Subscription: FC<Props> = ({
const basePlanCost = subscription?.tier.unit_amount / 100
const deriveTotalCost = (): number => {
const totalAddOnCost = sum(addOns.map((addOn) => addOn.unit_amount / 100))
const totalUsageCost = isPayg
? Number(sum(chargeableProducts.map((product) => deriveProductCost(paygStats, product))))
: 0
const totalAddOnCost = addOns
.map((addOn) => addOn.unit_amount / 100)
.reduce((prev, current) => prev + current, 0)
const totalUsageCost =
usage === undefined
? 0
: Object.keys(usage)
.map((productKey) => {
return usage[productKey].cost
})
.reduce((prev, current) => prev + current, 0)
return basePlanCost + totalAddOnCost + totalUsageCost
}
return (
<Loading active={loading}>
<Loading active={loading || loadingUsage}>
<div className="mb-8 w-full overflow-hidden rounded border border-panel-border-light dark:border-panel-border-dark">
<div className="bg-panel-body-light dark:bg-panel-body-dark">
<div className="flex items-center justify-between px-6 pt-4">
@@ -164,8 +170,8 @@ const Subscription: FC<Props> = ({
<CostBreakdownRow
item="Base Plan"
amount={1}
unitPrice={basePlanCost}
price={basePlanCost}
unitPrice={basePlanCost.toFixed(2)}
price={basePlanCost.toFixed(2)}
/>
{addOns.map((addOn) => (
<CostBreakdownRow
@@ -177,25 +183,40 @@ const Subscription: FC<Props> = ({
/>
))}
{isPayg &&
chargeableProducts.map((product) =>
product.features.map((feature: ProductFeature) => {
const derivedCost = deriveFeatureCost(paygStats, feature)
const cost = derivedCost.toFixed(2)
USAGE_BASED_PRODUCTS.map((product) => {
return product.features.map((feature) => {
const amount = usage?.[feature.key]?.usage ?? 0
const limit = usage?.[feature.key]?.limit ?? 0
const cost = (usage?.[feature.key]?.cost ?? 0).toFixed(2)
return (
<CostBreakdownRow
key={feature.attribute}
key={feature.key}
item={feature.title}
freeQuota={feature.freeQuota}
amount={formatBytes(
paygStats?.[feature.attribute]?.[feature.pricingModel] ?? 0
)}
unitPrice={`${feature.costPerUnit}/GB`}
amount={
feature.units === 'bytes' ? formatBytes(amount) : amount.toLocaleString()
}
unitPrice={
feature.units === 'bytes'
? `${feature.costPerUnit}/GB`
: feature.costPerUnit
}
price={cost}
note={
feature.units === 'bytes'
? `${formatBytes(limit)} included`
: `${limit.toLocaleString()} included`
}
/>
)
})
)}
<div className="relative flex items-center border-t border-panel-border-light px-6 py-3 dark:border-panel-border-dark">
})}
<div
className={[
'relative flex items-center border-t px-6 py-3',
'border-panel-border-light dark:border-panel-border-dark',
].join(' ')}
>
<div className="w-[80%]">
<p className="text-sm text-scale-1100">
Estimated cost for {dayjs.unix(currentPeriodStart).utc().format('MMM D, YYYY')}{' '}
@@ -12,7 +12,6 @@ import UpdateSuccess from './UpdateSuccess'
import Invoices from './Invoices'
export * from './Subscription/Subscription.types'
export * from './PAYGUsage/PAYGUsage.constants'
export {
PAYGUsage,
Subscription,
@@ -1,25 +0,0 @@
import { IconArchive, IconDatabase, IconKey } from 'ui'
export const usageBasedItems = [
{
title: 'Database',
icon: <IconDatabase className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{ key: 'db_size', title: 'Database space', units: 'bytes' },
{ key: 'db_egress', title: 'Database egress', units: 'bytes' },
],
},
{
title: 'Auth',
icon: <IconKey className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [{ key: 'monthly_active_users', title: 'Monthly Active Users', units: 'absolute' }],
},
{
title: 'Storage',
icon: <IconArchive className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{ key: 'storage_size', title: 'Storage space', units: 'bytes' },
{ key: 'storage_egress', title: 'Storage egress', units: 'bytes' },
],
},
]
@@ -6,8 +6,8 @@ import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
import SparkBar from 'components/ui/SparkBar'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { usageBasedItems } from './ProjectUsageBars.constants'
import InformationBox from 'components/ui/InformationBox'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
interface Props {
projectRef?: string
@@ -48,7 +48,7 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
<Loading active={isLoading}>
{usage && (
<div>
{usageBasedItems.map((product) => {
{USAGE_BASED_PRODUCTS.map((product) => {
const isExceededUsage =
showUsageExceedMessage &&
product.features
@@ -1,6 +1,7 @@
export interface ResourceUsage {
usage: number
limit: number
cost: number
}
export interface UsageStats {
@@ -5,7 +5,7 @@ import { useProjectSubscription, useProjectUsage } from 'hooks'
import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
import SparkBar from 'components/ui/SparkBar'
import { usageBasedItems } from './ProjectUsageBars.constants'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
interface ProjectUsageMinimalProps {
projectRef?: string
@@ -23,7 +23,7 @@ const ProjectUsageMinimal: FC<ProjectUsageMinimalProps> = ({ projectRef, filter
return <></>
}
const product = usageBasedItems.find((item) => item.title === filter)
const product = USAGE_BASED_PRODUCTS.find((item) => item.title === filter)
if (!product) return <></>
if (usageError || subscriptionError) {
@@ -0,0 +1,64 @@
import { createClient } from '@supabase/supabase-js'
import { uuidv4 } from 'lib/helpers'
const SUPPORT_API_URL = process.env.NEXT_PUBLIC_SUPPORT_API_URL || ''
const SUPPORT_API_KEY = process.env.NEXT_PUBLIC_SUPPORT_ANON_KEY || ''
export const convertB64toBlob = (image: string) => {
const contentType = 'image/png'
const byteCharacters = atob(image.substr(`data:${contentType};base64,`.length))
const byteArrays = []
for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
const slice = byteCharacters.slice(offset, offset + 1024)
const byteNumbers = new Array(slice.length)
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}
const blob = new Blob(byteArrays, { type: contentType })
return blob
}
export const uploadAttachment = async (ref: string, image: string) => {
const supabaseClient = createClient(SUPPORT_API_URL, SUPPORT_API_KEY, {
auth: {
persistSession: false,
autoRefreshToken: false,
// @ts-ignore
multiTab: false,
detectSessionInUrl: false,
localStorage: {
getItem: (key: string) => undefined,
setItem: (key: string, value: string) => {},
removeItem: (key: string) => {},
},
},
})
const blob = convertB64toBlob(image)
const name = `${ref || 'no-project'}/${uuidv4()}.png`
const options = { cacheControl: '3600' }
const { data: file, error: uploadError } = await supabaseClient.storage
.from('feedback-attachments')
.upload(name, blob, options)
if (uploadError) {
console.error('Failed to upload:', uploadError)
return undefined
}
if (file) {
const { data } = await supabaseClient.storage
.from('feedback-attachments')
.createSignedUrls([file.path], 10 * 365 * 24 * 60 * 60)
return data?.[0].signedUrl
}
return undefined
}
@@ -1,11 +1,14 @@
import { useState, useEffect, useRef, FC } from 'react'
import Link from 'next/link'
import { useState, useEffect, useRef, FC } from 'react'
import { useRouter } from 'next/router'
import { Button, Input, Popover } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { toPng } from 'html-to-image'
import { Button, Input, Popover, IconCamera, IconX } from 'ui'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { convertB64toBlob, uploadAttachment } from './FeedbackDropdown.utils'
interface Props {
onClose: () => void
@@ -19,7 +22,10 @@ const FeedbackWidget: FC<Props> = ({ onClose }) => {
const inputRef = useRef<any>(null)
const [feedback, setFeedback] = useState('')
const [screenshot, setScreenshot] = useState<string>()
const [isSending, setSending] = useState(false)
const [isSavingScreenshot, setIsSavingScreenshot] = useState(false)
useEffect(() => {
inputRef?.current?.focus()
@@ -29,15 +35,47 @@ const FeedbackWidget: FC<Props> = ({ onClose }) => {
setFeedback(e.target.value)
}
function onCancel() {
onClose()
const captureScreenshot = () => {
setIsSavingScreenshot(true)
function filter(node: HTMLElement) {
if ((node?.children ?? []).length > 0) {
return node.children[0].id !== 'feedback-widget'
}
return true
}
toPng(document.body, { filter })
.then((dataUrl: any) => setScreenshot(dataUrl))
.catch((error: any) => {
ui.setNotification({
error,
category: 'error',
message: 'Failed to capture screenshot',
duration: 4000,
})
})
.finally(() => {
setIsSavingScreenshot(false)
})
}
const sendFeedback = async () => {
if (feedback.length) {
if (feedback.length === 0 && screenshot !== undefined) {
return ui.setNotification({
category: 'error',
message: 'Please include a message in your feedback.',
duration: 4000,
})
} else if (feedback.length > 0) {
setSending(true)
const attachmentUrl = screenshot
? await uploadAttachment(ref as string, screenshot)
: undefined
const formattedFeedback =
attachmentUrl !== undefined ? `${feedback}\n\nAttachments:\n${attachmentUrl}` : feedback
await post(`${API_URL}/feedback/send`, {
message: feedback,
message: formattedFeedback,
pathname: router.asPath,
category: 'Feedback',
projectRef: ref,
@@ -52,7 +90,7 @@ const FeedbackWidget: FC<Props> = ({ onClose }) => {
}
return (
<div className="">
<div id="feedback-widget">
<Input.TextArea
className="w-80 p-3"
size="small"
@@ -64,12 +102,66 @@ const FeedbackWidget: FC<Props> = ({ onClose }) => {
<Popover.Seperator />
<div className="w-80 space-y-3 px-3 py-2 pb-4">
<div className="flex justify-between space-x-2">
<Button type="default" onClick={onCancel} className="hover:border-gray-500">
<Button type="default" onClick={onClose} className="hover:border-gray-500">
Cancel
</Button>
<Button disabled={isSending} loading={isSending} onClick={sendFeedback}>
Send feedback
</Button>
<div className="flex items-center space-x-2">
{screenshot !== undefined ? (
<div
style={{ backgroundImage: `url("${screenshot}")` }}
onClick={() => {
const blob = convertB64toBlob(screenshot)
const blobUrl = URL.createObjectURL(blob)
window.open(blobUrl, '_blank')
}}
className="cursor-pointer rounded h-[26px] w-[30px] border border-scale-600 relative bg-cover bg-center bg-no-repeat"
>
<div
className={[
'cursor-pointer rounded-full bg-red-900 h-3 w-3',
'flex items-center justify-center absolute -top-1 -right-1',
].join(' ')}
onClick={(event) => {
event.stopPropagation()
setScreenshot(undefined)
}}
>
<IconX size={8} strokeWidth={3} />
</div>
</div>
) : (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
as="span"
type="default"
disabled={isSavingScreenshot}
loading={isSavingScreenshot}
className="px-2 py-1.5"
icon={<IconCamera size={14} />}
onClick={() => captureScreenshot()}
/>
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'bg-scale-100 rounded py-1 px-2 leading-none shadow', // background
'w-[130px] text-center border-scale-200 border', //border
].join(' ')}
>
<span className="text-scale-1200 text-xs">
Capture screenshot of current view
</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
)}
<Button disabled={isSending} loading={isSending} onClick={sendFeedback}>
Send feedback
</Button>
</div>
</div>
<p className="text-xs text-scale-1000">
Have a technical issue? Contact{' '}
@@ -16,6 +16,7 @@ import {
IconEdit2,
IconLoader,
IconChevronsUp,
IconList,
} from 'ui'
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
import { STORAGE_VIEWS, STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER } from '../Storage.constants.ts'
@@ -284,11 +285,18 @@ const FileExplorerHeader = ({
>
<Button
as="span"
icon={<IconColumns size={16} strokeWidth={2} />}
icon={
view === 'LIST' ? (
<IconList size={16} strokeWidth={2} />
) : (
<IconColumns size={16} strokeWidth={2} />
)
}
type="text"
disabled={breadcrumbs.length === 0}
onChange={onChangeView}
>
Views
View as
</Button>
</Dropdown>
<Dropdown
+12 -2
View File
@@ -1,6 +1,13 @@
import { useRef, useEffect, useState, FormEvent, KeyboardEvent, ReactNode } from 'react'
import { orderBy, filter, without } from 'lodash'
import { Popover, IconCheck, IconAlertCircle, IconSearch, IconPlus } from 'ui'
import {
Popover,
IconCheck,
IconAlertCircle,
IconSearch,
IconPlus,
IconChevronDown,
} from 'ui'
import { BadgeDisabled, BadgeSelected } from './Badges'
@@ -223,7 +230,7 @@ export default function MultiSelect({
>
<div
className={[
'flex w-full flex-wrap items-start items-center gap-1.5 p-1.5',
'flex w-full flex-wrap items-start gap-1.5 p-1.5 cursor-default',
`${selectedOptions.length === 0 ? 'h-9' : ''}`,
].join(' ')}
>
@@ -250,6 +257,9 @@ export default function MultiSelect({
)
}
})}
<div className="absolute inset-y-0 right-0 pl-3 pr-1 flex space-x-1 items-center cursor-pointer ">
<IconChevronDown className="text-scale-900" />
</div>
</div>
</Popover>
</div>
-1
View File
@@ -5,7 +5,6 @@ export * from './useFreeProjectLimitCheck'
export * from './usePermissions'
export * from './useProfile'
export * from './useProjectAuthConfig'
export * from './useProjectPaygStatistics'
export * from './useProjectSettings'
export * from './useProjectSubscription'
export * from './useSubscriptionStats'
@@ -1,82 +0,0 @@
import dayjs from 'dayjs'
import useSWR from 'swr'
import { get as _get, maxBy } from 'lodash'
import { get } from 'lib/common/fetch'
import { API_URL, DATE_FORMAT, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { chargeableProducts } from 'components/interfaces/Billing'
import { PaygStats } from 'components/interfaces/Billing/PAYGUsage/PAYGUsage.types'
/**
* Get project payg statistics
*
* @param ref project reference
* @param tierProdId tier product supabase_prod_id
*/
export function useProjectPaygStatistics(ref?: string, tierProdId?: string) {
const startDate = dayjs().utc().startOf('month').format(DATE_FORMAT)
const endDate = dayjs().utc().endOf('month').format(DATE_FORMAT)
const attributes =
'total_egress,total_db_size_bytes,total_db_egress_bytes,total_storage_size_bytes,total_storage_egress'
const url = `${API_URL}/projects/${ref}/daily-stats?attribute=${attributes}&startDate=${encodeURIComponent(
startDate
)}&endDate=${encodeURIComponent(endDate)}&interval='1d'`
/**
* data is a dictionary
* {
* data: array,
* totalAverage: number,
* total: number,
* maximum: number,
* format: string,
* }
*/
const { data, error } = useSWR<any>(
ref && tierProdId === PRICING_TIER_PRODUCT_IDS.PAYG ? url : null,
get
)
const anyError = data?.error || error
return {
paygStats: anyError ? undefined : getPaygStats(data?.data),
error: anyError,
isLoading: !anyError && !data,
isError: !!anyError,
}
}
function getPaygStats(data: any): PaygStats {
const paygStats: any = {}
if (!data) return paygStats
let dataModified: any = []
data.map((x: any) => {
dataModified.push({
...x,
total_egress_modified:
(x.total_egress == undefined ? 0 : x.total_egress) -
(x.total_storage_egress == undefined ? 0 : x.total_storage_egress),
})
})
chargeableProducts.forEach((product) => {
product.features.forEach((feature) => {
// calculate max
const max = _get(maxBy(dataModified, feature.attribute), feature.attribute) ?? 0
// calculate sum
let sum = 0
dataModified.map((x: any) => {
const featureAttribute = x[feature.attribute] == undefined ? 0 : x[feature.attribute]
sum = sum + featureAttribute
})
// set sum and max under attribute name
paygStats[feature.attribute] = {
sum,
max,
}
})
})
return paygStats
}
+1 -1
View File
@@ -29,7 +29,7 @@ export default async function apiWrapper(
}
}
const func = withSentry(handler)
const func = withSentry(handler as any)
// @ts-ignore
return await func(req, res)
} catch (error) {
+2 -1
View File
@@ -32,13 +32,14 @@
"analytics-node": "^3.5.0",
"awesome-debounce-promise": "^2.1.0",
"clipboard": "^2.0.8",
"config": "*",
"common": "*",
"config": "*",
"configcat-js": "^5.6.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"json-logic-js": "^2.0.2",
"lodash": "^4.17.21",
+1
View File
@@ -10,6 +10,7 @@ import 'styles/contextMenu.scss'
import 'styles/react-data-grid-logs.scss'
import 'styles/date-picker.scss'
import 'styles/grid.scss'
import 'styles/users-table.scss'
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
@@ -4,14 +4,13 @@ import { observer } from 'mobx-react-lite'
import { Loading, IconArrowRight } from 'ui'
import { Project, NextPageWithLayout } from 'types'
import { useProjectPaygStatistics, useProjectSubscription, useStore } from 'hooks'
import { useProjectSubscription, useStore } from 'hooks'
import { STRIPE_PRODUCT_IDS, TIME_PERIODS_REPORTS, TIME_PERIODS_BILLING } from 'lib/constants'
import { SettingsLayout } from 'components/layouts'
import LoadingUI from 'components/ui/Loading'
import OveragesBanner from 'components/ui/OveragesBanner/OveragesBanner'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import { PAYGUsage, Subscription, Invoices } from 'components/interfaces/Billing'
import { PaygStats } from 'components/interfaces/Billing/PAYGUsage/PAYGUsage.types'
import ProjectUsage from 'components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars'
const ProjectBilling: NextPageWithLayout = () => {
@@ -39,9 +38,6 @@ interface SettingsProps {
const Settings: FC<SettingsProps> = ({ project }) => {
const { ui } = useStore()
const projectTier = ui.selectedProject?.subscription_tier
const [dateRange, setDateRange] = useState<any>()
const {
subscription,
@@ -49,10 +45,9 @@ const Settings: FC<SettingsProps> = ({ project }) => {
error,
} = useProjectSubscription(ui.selectedProject?.ref)
const { paygStats }: { paygStats: PaygStats | undefined } = useProjectPaygStatistics(
ui.selectedProject?.ref,
subscription?.tier?.supabase_prod_id
)
const [dateRange, setDateRange] = useState<any>()
const projectTier = ui.selectedProject?.subscription_tier
const isPayg = subscription?.tier?.prod_id === STRIPE_PRODUCT_IDS.PAYG
useEffect(() => {
if (error) {
@@ -74,7 +69,6 @@ const Settings: FC<SettingsProps> = ({ project }) => {
loading={loading}
project={project}
subscription={subscription}
paygStats={paygStats}
currentPeriodStart={subscription?.billing.current_period_start}
currentPeriodEnd={subscription?.billing.current_period_end}
/>
@@ -86,7 +80,7 @@ const Settings: FC<SettingsProps> = ({ project }) => {
</div>
</div>
</Loading>
) : subscription?.tier?.prod_id === STRIPE_PRODUCT_IDS.PAYG ? (
) : isPayg ? (
<div>
<div className="mb-4 flex items-center space-x-3">
<DateRangePicker
@@ -110,7 +104,7 @@ const Settings: FC<SettingsProps> = ({ project }) => {
</div>
)}
</div>
{paygStats && dateRange && <PAYGUsage paygStats={paygStats} dateRange={dateRange} />}
{dateRange && <PAYGUsage dateRange={dateRange} />}
</div>
) : (
<ProjectUsage projectRef={project?.ref} />
+13 -8
View File
@@ -32,12 +32,17 @@ span.th.tr.td.textarea {
}
.grid-monaco-editor,
.monaco-editor .suggest-widget .main {
max-height: none;
height: inherit;
width: 100%;
margin: inherit;
padding: inherit;
overflow: inherit;
background-color: inherit;
.monaco-editor .suggest-widget {
.main {
max-height: none;
height: inherit;
width: 100%;
margin: inherit;
padding: inherit;
overflow: inherit;
}
.monaco-list-row.focused .main {
background: var(--vscode-quickInput-background);
}
}
+39
View File
@@ -0,0 +1,39 @@
.users-table-container {
scrollbar-width: thin;
scrollbar-color: var(--colors-slate9) var(--colors-slate1);
.dark & {
scrollbar-color: var(--colors-slate10) var(--colors-slate3o);
}
::-webkit-scrollbar {
background: var(--colors-slate2);
height: 3px;
width: 0px;
border-radius: 0;
.dark & {
background: var(--colors-slate6);
}
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px var(--colors-slate6);
-webkit-border-radius: 5px;
border-radius: 5px;
.dark & {
-webkit-box-shadow: inset 0 0 6px var(--colors-slate8);
}
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 5px;
border-radius: 5px;
background: var(--colors-slate9);
.dark & {
-webkit-box-shadow: inset 0 0 6px var(--colors-slate12);
}
}
}