mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
Merge branch 'master' into feat/sso-self-serve
This commit is contained in:
78 files changed
+985
-21644
No files matched your search
@@ -1,16 +1,16 @@
|
||||
## Project setup
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase and then creating a "schema" inside the database.
|
||||
Before you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. [Create a new project](https://supabase.com/dashboard) in the Supabase Dashboard.
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
2. Enter your project details.
|
||||
3. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor, or you can just copy/paste the SQL from below and run it yourself.
|
||||
Now set up the database schema. You can use the "User Management Starter" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -29,7 +29,7 @@ Now we are going to set up the database schema. We can use the "User Management
|
||||
|
||||
You can pull the database schema down to your local project by running the `db pull` command. Read the [local development docs](/docs/guides/cli/local-development#link-your-project) for detailed instructions.
|
||||
|
||||
</Admonition>
|
||||
{/* TODO: discuss */}
|
||||
|
||||
```bash
|
||||
supabase link --project-ref <project-id>
|
||||
@@ -37,6 +37,8 @@ supabase link --project-ref <project-id>
|
||||
supabase db pull
|
||||
```
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
@@ -58,7 +60,8 @@ supabase migration new user_management_starter
|
||||
### Get the API keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the Project URL and `anon` key from the API settings.
|
||||
|
||||
To do this, you need to get the Project URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [API Settings](https://supabase.com/dashboard/project/_/settings/api) page in the Dashboard.
|
||||
1. Find your Project `URL`, `anon`, and `service_role` keys on this page.
|
||||
2. Find your Project `URL`, `anon`, and `service_role` keys on this page.
|
||||
@@ -960,11 +960,11 @@ Consider configuring a custom SMTP server for production.
|
||||
|
||||
See the [Custom SMTP guide](/docs/guides/auth/auth-smtp) for instructions.
|
||||
|
||||
#### Local development with Inbucket
|
||||
#### Local development with Mailpit
|
||||
|
||||
You can test email flows on your local machine. The Supabase CLI automatically captures emails sent locally by using [Inbucket](https://github.com/inbucket/inbucket).
|
||||
You can test email flows on your local machine. The Supabase CLI automatically captures emails sent locally by using [Mailpit](https://github.com/axllent/mailpit).
|
||||
|
||||
In your terminal, run `supabase status` to get the Inbucket URL. Go to this URL in your browser, and follow the instructions to find your emails.
|
||||
In your terminal, run `supabase status` to get the Mailpit URL. Go to this URL in your browser, and follow the instructions to find your emails.
|
||||
|
||||
## With phone
|
||||
|
||||
|
||||
@@ -17,27 +17,27 @@ If you get stuck while working through this guide, refer to the [full example on
|
||||
|
||||
## Building the app
|
||||
|
||||
Let's start building the Angular app from scratch.
|
||||
Start with building the Angular app from scratch.
|
||||
|
||||
### Initialize an Angular app
|
||||
|
||||
We can use the [Angular CLI](https://angular.io/cli) to initialize
|
||||
an app called `supabase-angular`:
|
||||
You can use the [Angular CLI](https://angular.io/cli) to initialize
|
||||
an app called `supabase-angular`. The command sets some defaults, that you change to suit your needs:
|
||||
|
||||
```bash
|
||||
npx ng new supabase-angular --routing false --style css --standalone false
|
||||
npx ng new supabase-angular --routing false --style css --standalone false --zoneless true --ssr false
|
||||
cd supabase-angular
|
||||
```
|
||||
|
||||
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
Then, install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in the `src/environments/environment.ts` file.
|
||||
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
Finally, save the environment variables in the `src/environments/environment.ts` file.
|
||||
All you need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
The application exposes these variables in the browser, and that's fine as you have [Row Level Security](/docs/guides/auth#row-level-security) enabled on the Database.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
@@ -51,7 +51,7 @@ export const environment = {
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Now that we have the API credentials in place, let's create a `SupabaseService` with `ng g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API.
|
||||
Now you have the API credentials in place, create a `SupabaseService` with `ng g s supabase` and add the following code to initialize the Supabase client and implement functions to communicate with the Supabase API.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
@@ -133,37 +133,41 @@ export class SupabaseService {
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Optionally, update [src/styles.css](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/angular-user-management/src/styles.css) to style the app.
|
||||
Optionally, update `src/styles.css` [with the following styles](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/angular-user-management/src/styles.css) to style the app.
|
||||
|
||||
### Set up a login component
|
||||
|
||||
Let's set up an Angular component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
|
||||
Create an `AuthComponent` with `ng g c auth` Angular CLI command.
|
||||
Next, set up an Angular component to manage logins and sign ups. The component uses [Magic Links](/docs/guides/auth/auth-email-passwordless#with-magic-link), so users can sign in with their email without using passwords.
|
||||
|
||||
Create an `AuthComponent` with the `ng g c auth` Angular CLI command and add the following code.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/auth/auth.component.ts
|
||||
```ts name=src/app/auth/auth.ts
|
||||
import { Component } from '@angular/core'
|
||||
import { FormBuilder } from '@angular/forms'
|
||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth',
|
||||
templateUrl: './auth.component.html',
|
||||
styleUrls: ['./auth.component.css'],
|
||||
templateUrl: './auth.html',
|
||||
styleUrls: ['./auth.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AuthComponent {
|
||||
loading = false
|
||||
|
||||
signInForm = this.formBuilder.group({
|
||||
email: '',
|
||||
})
|
||||
|
||||
signInForm!: FormGroup
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
loading = false
|
||||
ngOnInit() {
|
||||
this.signInForm = this.formBuilder.group({
|
||||
email: '',
|
||||
})
|
||||
}
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
try {
|
||||
this.loading = true
|
||||
@@ -183,7 +187,7 @@ export class AuthComponent {
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/auth/auth.component.html
|
||||
```html name=src/app/auth/auth.html
|
||||
<div class="row flex-center flex">
|
||||
<div class="col-6 form-widget" aria-live="polite">
|
||||
<h1 class="header">Supabase + Angular</h1>
|
||||
@@ -201,7 +205,7 @@ export class AuthComponent {
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button block" [disabled]="loading">
|
||||
{{ loading ? 'Loading' : 'Send magic link' }}
|
||||
{{ loading ? "Loading" : "Send magic link" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -214,38 +218,50 @@ export class AuthComponent {
|
||||
### Account page
|
||||
|
||||
Users also need a way to edit their profile details and manage their accounts after signing in.
|
||||
Create an `AccountComponent` with the `ng g c account` Angular CLI command.
|
||||
Create an `AccountComponent` with the `ng g c account` Angular CLI command and add the following code.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/account/account.component.ts
|
||||
```ts name=src/app/account/account.ts
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { FormBuilder } from '@angular/forms'
|
||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||
import { AuthSession } from '@supabase/supabase-js'
|
||||
import { Profile, SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.component.html',
|
||||
styleUrls: ['./account.component.css'],
|
||||
templateUrl: './account.html',
|
||||
styleUrls: ['./account.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
loading = false
|
||||
profile!: Profile
|
||||
updateProfileForm!: FormGroup
|
||||
|
||||
get avatarUrl() {
|
||||
return this.updateProfileForm.value.avatar_url as string
|
||||
}
|
||||
async updateAvatar(event: string): Promise<void> {
|
||||
this.updateProfileForm.patchValue({
|
||||
avatar_url: event,
|
||||
})
|
||||
await this.updateProfile()
|
||||
}
|
||||
|
||||
@Input()
|
||||
session!: AuthSession
|
||||
|
||||
updateProfileForm = this.formBuilder.group({
|
||||
username: '',
|
||||
website: '',
|
||||
avatar_url: '',
|
||||
})
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private formBuilder: FormBuilder
|
||||
) {}
|
||||
) {
|
||||
this.updateProfileForm = this.formBuilder.group({
|
||||
username: '',
|
||||
website: '',
|
||||
avatar_url: '',
|
||||
})
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.getProfile()
|
||||
@@ -311,8 +327,9 @@ export class AccountComponent implements OnInit {
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/account/account.component.html
|
||||
```html name=src/app/account/account.html
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"> </app-avatar>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" [value]="session.user.email" disabled />
|
||||
@@ -328,7 +345,7 @@ export class AccountComponent implements OnInit {
|
||||
|
||||
<div>
|
||||
<button type="submit" class="button primary block" [disabled]="loading">
|
||||
{{ loading ? 'Loading ...' : 'Update' }}
|
||||
{{ loading ? "Loading ..." : "Update" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -342,33 +359,34 @@ export class AccountComponent implements OnInit {
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `AppComponent`:
|
||||
Now you have all the components in place, update `AppComponent`:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/app.component.ts
|
||||
```ts name=src/app/app.ts
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { SupabaseService } from './supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css'],
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ['./app.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
title = 'angular-user-management'
|
||||
|
||||
session = this.supabase.session
|
||||
|
||||
constructor(private readonly supabase: SupabaseService) {}
|
||||
|
||||
title = 'angular-user-management'
|
||||
session: any
|
||||
|
||||
ngOnInit() {
|
||||
this.session = this.supabase.session
|
||||
this.supabase.authChanges((_, session) => (this.session = session))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/app.component.html
|
||||
```html name=src/app/app.html
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<app-account *ngIf="session; else auth" [session]="session"></app-account>
|
||||
<ng-template #auth>
|
||||
@@ -379,7 +397,7 @@ export class AppComponent implements OnInit {
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
`app.module.ts` also needs to be modified to include the `ReactiveFormsModule` from the `@angular/forms` package.
|
||||
You also need to change `app.module.ts` to include the `ReactiveFormsModule` from the `@angular/forms` package.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
@@ -387,32 +405,33 @@ export class AppComponent implements OnInit {
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
|
||||
import { AppComponent } from './app.component'
|
||||
import { AuthComponent } from './auth/auth.component'
|
||||
import { AccountComponent } from './account/account.component'
|
||||
import { AppComponent } from './app'
|
||||
import { AuthComponent } from './auth/auth'
|
||||
import { AccountComponent } from './account/account'
|
||||
import { ReactiveFormsModule } from '@angular/forms'
|
||||
import { AvatarComponent } from './avatar/avatar.component'
|
||||
import { AvatarComponent } from './avatar/avatar'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
|
||||
imports: [BrowserModule, ReactiveFormsModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
exports: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
Once that's done, run the application in a terminal:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
And then open the browser to [localhost:4200](http://localhost:4200) and you should see the completed app.
|
||||
Open the browser to [localhost:4200](http://localhost:4200) and you should see the completed app.
|
||||
|
||||

|
||||

|
||||
|
||||
## Bonus: Profile photos
|
||||
|
||||
@@ -420,20 +439,21 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Let's create an avatar for the user so that they can upload a profile photo.
|
||||
Create an `AvatarComponent` with `ng g c avatar` Angular CLI command.
|
||||
Create an avatar for the user so that they can upload a profile photo.
|
||||
Create an `AvatarComponent` with `ng g c avatar` Angular CLI command and add the following code.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/avatar/avatar.component.ts
|
||||
```ts name=src/app/avatar/avatar.ts
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-avatar',
|
||||
templateUrl: './avatar.component.html',
|
||||
styleUrls: ['./avatar.component.css'],
|
||||
templateUrl: './avatar.html',
|
||||
styleUrls: ['./avatar.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AvatarComponent {
|
||||
_avatarUrl: SafeResourceUrl | undefined
|
||||
@@ -490,7 +510,7 @@ export class AvatarComponent {
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/avatar/avatar.component.html
|
||||
```html name=src/app/avatar/avatar.html
|
||||
<div>
|
||||
<img
|
||||
*ngIf="_avatarUrl"
|
||||
@@ -503,10 +523,10 @@ export class AvatarComponent {
|
||||
<div *ngIf="!_avatarUrl" class="avatar no-image" style="height: 150px; width: 150px"></div>
|
||||
<div style="width: 150px">
|
||||
<label class="button primary block" for="single">
|
||||
{{ uploading ? 'Uploading ...' : 'Upload' }}
|
||||
{{ uploading ? "Uploading ..." : "Upload" }}
|
||||
</label>
|
||||
<input
|
||||
style="visibility: hidden;position: absolute"
|
||||
style="visibility: hidden; position: absolute"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
@@ -524,9 +544,9 @@ And then we can add the widget on top of the `AccountComponent` HTML template:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/app/account.component.html
|
||||
```html name=src/app/account.html
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"> </app-avatar>
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"></app-avatar>
|
||||
<!-- input fields -->
|
||||
</form>
|
||||
```
|
||||
@@ -537,11 +557,11 @@ And add an `updateAvatar` function along with an `avatarUrl` getter to the `Acco
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/account.component.ts
|
||||
```ts name=src/app/account.ts
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.component.html',
|
||||
styleUrls: ['./account.component.css'],
|
||||
templateUrl: './account.html',
|
||||
styleUrls: ['./account.css'],
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
// ...
|
||||
|
||||
@@ -5,6 +5,17 @@ subtitle: Learn how to develop locally and use the Supabase CLI
|
||||
|
||||
Develop locally while running the Supabase stack on your machine.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
As a prerequisite, you must install a container runtime compatible with Docker APIs.
|
||||
|
||||
- [Docker Desktop](https://docs.docker.com/desktop/) (macOS, Windows, Linux)
|
||||
- [Rancher Desktop](https://rancherdesktop.io/) (macOS, Windows, Linux)
|
||||
- [Podman](https://podman.io/) (macOS, Windows, Linux)
|
||||
- [OrbStack](https://orbstack.dev/) (macOS)
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Quickstart
|
||||
|
||||
1. Install the Supabase CLI:
|
||||
@@ -101,17 +112,6 @@ Develop locally while running the Supabase stack on your machine.
|
||||
|
||||
</Tabs>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
As a prerequisite, you must install a container runtime compatible with Docker APIs.
|
||||
|
||||
- [Docker Desktop](https://docs.docker.com/desktop/) (macOS, Windows, Linux)
|
||||
- [Rancher Desktop](https://rancherdesktop.io/) (macOS, Windows, Linux)
|
||||
- [Podman](https://podman.io/) (macOS, Windows, Linux)
|
||||
- [OrbStack](https://orbstack.dev/) (macOS)
|
||||
|
||||
</Admonition>
|
||||
|
||||
4. View your local Supabase instance at [http://localhost:54323](http://localhost:54323).
|
||||
|
||||
## Local development
|
||||
|
||||
@@ -217,7 +217,7 @@ Started supabase local development setup.
|
||||
API URL: http://localhost:54321
|
||||
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
|
||||
Studio URL: http://localhost:54323
|
||||
Inbucket URL: http://localhost:54324
|
||||
Mailpit URL: http://localhost:54324
|
||||
anon key: eyJh......
|
||||
service_role key: eyJh......
|
||||
|
||||
|
||||
@@ -36,11 +36,11 @@ Edge Functions are powered by Deno, which provides a [native set of testing tool
|
||||
|
||||
## Testing Auth emails
|
||||
|
||||
The Supabase CLI uses [Inbucket](https://github.com/inbucket/inbucket) to capture emails sent from your local machine. This is useful for testing emails sent from Supabase Auth.
|
||||
The Supabase CLI uses [Mailpit](https://github.com/axllent/mailpit) to capture emails sent from your local machine. This is useful for testing emails sent from Supabase Auth.
|
||||
|
||||
### Accessing Inbucket
|
||||
### Accessing Mailpit
|
||||
|
||||
By default, Inbucket is available at [localhost:54324](http://localhost:54324) when you run `supabase start`. Open this URL in your browser to view the emails.
|
||||
By default, Mailpit is available at [localhost:54324](http://localhost:54324) when you run `supabase start`. Open this URL in your browser to view the emails.
|
||||
|
||||
### Going into production
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ As an example, if you start a Pro Plan subscription on January 1 and downgrade t
|
||||
|
||||
## Credit top-ups
|
||||
|
||||
You can top up credits at any time, with a maximum of <Price price="999" /> per top-up. These credits do not expire and are non-refundable.
|
||||
You can top up credits at any time, with a maximum of <Price price="2000" /> per top-up. These credits do not expire and are non-refundable.
|
||||
|
||||
If you have any outstanding invoices, we’ll automatically use your credits to pay them off. Any remaining credits will be applied to future invoices.
|
||||
|
||||
|
||||
@@ -25,6 +25,14 @@ This is all the information you need to connect to Supabase Storage using any S3
|
||||
|
||||
<img alt="Storage S3 Access keys" src="/docs/img/storage/s3-credentials.png" width="100%" />
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
For optimal performance when uploading large files you should always use the direct storage hostname. This provides several performance enhancements that will greatly improve performance when uploading large files.
|
||||
|
||||
Instead of `https://project-id.supabase.co` use `https://project-id.storage.supabase.co`
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
@@ -39,7 +47,7 @@ This is all the information you need to connect to Supabase Storage using any S3
|
||||
const client = new S3Client({
|
||||
forcePathStyle: true,
|
||||
region: 'project_region',
|
||||
endpoint: 'https://project_ref.supabase.co/storage/v1/s3',
|
||||
endpoint: 'https://project_ref.storage.supabase.co/storage/v1/s3',
|
||||
credentials: {
|
||||
accessKeyId: 'your_access_key_id',
|
||||
secretAccessKey: 'your_secret_access_key',
|
||||
@@ -55,7 +63,7 @@ This is all the information you need to connect to Supabase Storage using any S3
|
||||
[supabase]
|
||||
aws_access_key_id = your_access_key_id
|
||||
aws_secret_access_key = your_secret_access_key
|
||||
endpoint_url = https://project_ref.supabase.co/storage/v1/s3
|
||||
endpoint_url = https://project_ref.storage.supabase.co/storage/v1/s3
|
||||
region = project_region
|
||||
```
|
||||
</TabPanel>
|
||||
@@ -86,7 +94,7 @@ const {
|
||||
const client = new S3Client({
|
||||
forcePathStyle: true,
|
||||
region: 'project_region',
|
||||
endpoint: 'https://project_ref.supabase.co/storage/v1/s3',
|
||||
endpoint: 'https://project_ref.storage.supabase.co/storage/v1/s3',
|
||||
credentials: {
|
||||
accessKeyId: 'project_ref',
|
||||
secretAccessKey: 'anonKey',
|
||||
|
||||
@@ -14,6 +14,14 @@ The resumable upload method is recommended when:
|
||||
|
||||
Supabase Storage implements the [TUS protocol](https://tus.io/) to enable resumable uploads. TUS stands for The Upload Server and is an open protocol for supporting resumable uploads. The protocol allows the upload process to be resumed from where it left off in case of interruptions. This method can be implemented using the [`tus-js-client`](https://github.com/tus/tus-js-client) library, or other client-side libraries like [Uppy](https://uppy.io/docs/tus/) that support the TUS protocol.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
For optimal performance when uploading large files you should always use the direct storage hostname. This provides several performance enhancements that will greatly improve performance when uploading large files.
|
||||
|
||||
Instead of `https://project-id.supabase.co` use `https://project-id.storage.supabase.co`
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
@@ -35,7 +43,8 @@ Supabase Storage implements the [TUS protocol](https://tus.io/) to enable resuma
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
var upload = new tus.Upload(file, {
|
||||
endpoint: `https://${projectId}.supabase.co/storage/v1/upload/resumable`,
|
||||
// Supabase TUS endpoint (with direct storage hostname)
|
||||
endpoint: `https://${projectId}.storage.supabase.co/storage/v1/upload/resumable`,
|
||||
retryDelays: [0, 3000, 5000, 10000, 20000],
|
||||
headers: {
|
||||
authorization: `Bearer ${session.access_token}`,
|
||||
@@ -125,7 +134,7 @@ Supabase Storage implements the [TUS protocol](https://tus.io/) to enable resuma
|
||||
// Initialize Uppy instance only once
|
||||
const [uppy] = useState(() => new Uppy());
|
||||
// Initialize Supabase client with project URL and anon key
|
||||
const supabase = createClient(projectURL, anonKey);
|
||||
const supabase = createClient(`https://${projectId}.supabase.co`, anonKey);
|
||||
|
||||
useEffect(() => {
|
||||
const initializeUppy = async () => {
|
||||
@@ -135,7 +144,8 @@ Supabase Storage implements the [TUS protocol](https://tus.io/) to enable resuma
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
uppy.use(Tus, {
|
||||
endpoint: `${projectURL}/storage/v1/upload/resumable`, // Supabase TUS endpoint
|
||||
// Supabase TUS endpoint (with direct storage hostname)
|
||||
endpoint: `https://${projectId}.storage.supabase.co/storage/v1/upload/resumable`,
|
||||
retryDelays: [0, 3000, 5000, 10000, 20000], // Retry delays for resumable uploads
|
||||
headers: {
|
||||
authorization: `Bearer ${session?.access_token}`, // User session access token
|
||||
|
||||
@@ -42,13 +42,16 @@ import {
|
||||
Form_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
Label_Shadcn_ as Label,
|
||||
Switch,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
|
||||
export const CreateBranchModal = () => {
|
||||
const allowDataBranching = useFlag('allowDataBranching')
|
||||
const { ref } = useParams()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -141,6 +144,7 @@ export const CreateBranchModal = () => {
|
||||
(val) => gitlessBranching || !githubConnection || (val && val.length > 0),
|
||||
'Git branch name is required when GitHub is connected'
|
||||
),
|
||||
withData: z.boolean().default(false).optional(),
|
||||
})
|
||||
.superRefine(async (val, ctx) => {
|
||||
if (val.gitBranchName && val.gitBranchName.length > 0 && githubConnection?.repository.id) {
|
||||
@@ -164,7 +168,7 @@ export const CreateBranchModal = () => {
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onBlur',
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: { branchName: '', gitBranchName: '' },
|
||||
defaultValues: { branchName: '', gitBranchName: '', withData: false },
|
||||
})
|
||||
|
||||
const canSubmit = !isCreating && !isChecking
|
||||
@@ -184,6 +188,7 @@ export const CreateBranchModal = () => {
|
||||
branchName: data.branchName,
|
||||
is_default: false,
|
||||
...(data.gitBranchName ? { gitBranch: data.gitBranchName } : {}),
|
||||
...(allowDataBranching ? { withData: data.withData } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,6 +300,7 @@ export const CreateBranchModal = () => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoadingConnections && <GenericSkeletonLoader />}
|
||||
{isErrorConnections && (
|
||||
<AlertError
|
||||
@@ -315,7 +321,7 @@ export const CreateBranchModal = () => {
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light">
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Keep this preview branch in sync with a chosen GitHub branch
|
||||
</p>
|
||||
</div>
|
||||
@@ -326,6 +332,23 @@ export const CreateBranchModal = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{allowDataBranching && (
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="withData"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Include data"
|
||||
layout="flex-row-reverse"
|
||||
description="Clone production data into this branch"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</DialogSection>
|
||||
|
||||
<DialogSectionSeparator />
|
||||
|
||||
@@ -48,8 +48,8 @@ const FORM_ID = 'credit-top-up'
|
||||
const FormSchema = z.object({
|
||||
amount: z.coerce
|
||||
.number()
|
||||
.gte(100, 'Amount must be between $100 - $999.')
|
||||
.lte(999, 'Amount must be between $100 - $999.')
|
||||
.gte(100, 'Amount must be between $100 - $2000.')
|
||||
.lte(2000, 'Amount must be between $100 - $2000.')
|
||||
.int('Amount must be a whole number.'),
|
||||
paymentMethod: z.string(),
|
||||
})
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
* This component acts as a bridge between the data-fetching logic and the
|
||||
* presentational chart component.
|
||||
*/
|
||||
import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
|
||||
import LogChartHandler from 'components/ui/Charts/LogChartHandler'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted'
|
||||
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useChartData } from 'hooks/useChartData'
|
||||
import type { UpdateDateRange } from 'pages/project/[ref]/reports/database'
|
||||
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import Link from 'next/link'
|
||||
import { Button, cn } from 'ui'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
const ReportChart = ({
|
||||
chart,
|
||||
@@ -25,9 +28,7 @@ const ReportChart = ({
|
||||
interval,
|
||||
updateDateRange,
|
||||
functionIds,
|
||||
orgPlanId,
|
||||
isLoading,
|
||||
availableIn,
|
||||
className,
|
||||
}: {
|
||||
chart: any
|
||||
@@ -36,15 +37,17 @@ const ReportChart = ({
|
||||
interval: string
|
||||
updateDateRange: UpdateDateRange
|
||||
functionIds?: string[]
|
||||
orgPlanId?: string
|
||||
isLoading?: boolean
|
||||
availableIn?: string[]
|
||||
className?: string
|
||||
}) => {
|
||||
const org = useSelectedOrganization()
|
||||
const { plan: orgPlan } = useCurrentOrgPlan()
|
||||
const orgPlanId = orgPlan?.id
|
||||
|
||||
const [isHoveringUpgrade, setIsHoveringUpgrade] = useState(false)
|
||||
const isAvailable =
|
||||
chart.availableIn === undefined || (orgPlanId && chart.availableIn.includes(orgPlanId))
|
||||
|
||||
const canFetch = orgPlanId !== undefined
|
||||
const {
|
||||
data,
|
||||
@@ -114,7 +117,9 @@ const ReportChart = ({
|
||||
<h2 className="">{chart.label}</h2>
|
||||
<p className="text-sm text-foreground-light">
|
||||
This chart is available from{' '}
|
||||
<span className="capitalize">{!!availableIn?.length ? availableIn[0] : 'Pro'}</span>{' '}
|
||||
<span className="capitalize">
|
||||
{!!chart.availableIn?.length ? chart.availableIn[0] : 'Pro'}
|
||||
</span>{' '}
|
||||
plan and above
|
||||
</p>
|
||||
<Button
|
||||
@@ -125,7 +130,9 @@ const ReportChart = ({
|
||||
>
|
||||
<Link href={`/org/${org?.slug}/billing?panel=subscriptionPlan&source=reports`}>
|
||||
Upgrade to{' '}
|
||||
<span className="capitalize">{!!availableIn?.length ? availableIn[0] : 'Pro'}</span>
|
||||
<span className="capitalize">
|
||||
{!!chart.availableIn?.length ? chart.availableIn[0] : 'Pro'}
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -60,10 +60,13 @@ const ConfirmResetCodeForm = ({ email }: { email: string }) => {
|
||||
if (user?.factors?.length) {
|
||||
await router.push({
|
||||
pathname: '/forgot-password-mfa',
|
||||
query: { returnTo: '/reset-password' },
|
||||
query: router.query,
|
||||
})
|
||||
} else {
|
||||
await router.push('reset-password')
|
||||
await router.push({
|
||||
pathname: '/reset-password',
|
||||
query: router.query,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { auth } from 'lib/gotrue'
|
||||
import { auth, getReturnToPath } from 'lib/gotrue'
|
||||
import {
|
||||
Button,
|
||||
Form_Shadcn_,
|
||||
@@ -67,7 +67,7 @@ const ResetPasswordForm = () => {
|
||||
|
||||
// logout all other sessions after changing password
|
||||
await auth.signOut({ scope: 'others' })
|
||||
await router.push('/organizations')
|
||||
await router.push(getReturnToPath('/organizations'))
|
||||
} else {
|
||||
toast.error(`Failed to save password: ${error.message}`, { id: toastId })
|
||||
if (!WHITELIST_ERRORS.some((e) => error.message.includes(e))) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AuthError } from '@supabase/supabase-js'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { object, string } from 'yup'
|
||||
|
||||
@@ -28,10 +28,22 @@ const SignInForm = () => {
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
const [returnTo, setReturnTo] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Only call getReturnToPath after component mounts client-side
|
||||
setReturnTo(getReturnToPath())
|
||||
}, [])
|
||||
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const { mutate: addLoginEvent } = useAddLoginEvent()
|
||||
|
||||
let forgotPasswordUrl = `/forgot-password`
|
||||
|
||||
if (returnTo && !returnTo.includes('/forgot-password')) {
|
||||
forgotPasswordUrl = `${forgotPasswordUrl}?returnTo=${encodeURIComponent(returnTo)}`
|
||||
}
|
||||
|
||||
const onSignIn = async ({ email, password }: { email: string; password: string }) => {
|
||||
const toastId = toast.loading('Signing in...')
|
||||
|
||||
@@ -68,9 +80,12 @@ const SignInForm = () => {
|
||||
addLoginEvent({})
|
||||
|
||||
await queryClient.resetQueries()
|
||||
const returnTo = getReturnToPath()
|
||||
// since we're already on the /sign-in page, prevent redirect loops
|
||||
router.push(returnTo === '/sign-in' ? '/organizations' : returnTo)
|
||||
let redirectPath = '/organizations'
|
||||
if (returnTo && returnTo !== '/sign-in') {
|
||||
redirectPath = returnTo
|
||||
}
|
||||
router.push(redirectPath)
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to sign in: ${(error as AuthError).message}`, { id: toastId })
|
||||
Sentry.captureMessage('[CRITICAL] Failed to sign in via EP: ' + error.message)
|
||||
@@ -124,7 +139,7 @@ const SignInForm = () => {
|
||||
|
||||
{/* positioned using absolute instead of labelOptional prop so tabbing between inputs works smoothly */}
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
href={forgotPasswordUrl}
|
||||
className="absolute top-0 right-0 text-sm text-foreground-lighter"
|
||||
>
|
||||
Forgot Password?
|
||||
|
||||
@@ -18,7 +18,11 @@ const signInSchema = object({
|
||||
code: string().required('MFA Code is required'),
|
||||
})
|
||||
|
||||
const SignInMfaForm = () => {
|
||||
interface SignInMfaFormProps {
|
||||
context?: 'forgot-password' | 'sign-in'
|
||||
}
|
||||
|
||||
const SignInMfaForm = ({ context = 'sign-in' }: SignInMfaFormProps) => {
|
||||
const router = useRouter()
|
||||
const signOut = useSignOut()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -38,7 +42,15 @@ const SignInMfaForm = () => {
|
||||
} = useMfaChallengeAndVerifyMutation({
|
||||
onSuccess: async () => {
|
||||
await queryClient.resetQueries()
|
||||
router.push(getReturnToPath())
|
||||
|
||||
if (context === 'forgot-password') {
|
||||
router.push({
|
||||
pathname: '/reset-password',
|
||||
query: router.query,
|
||||
})
|
||||
} else {
|
||||
router.push(getReturnToPath())
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { AppBannerWrapper } from 'components/interfaces/App'
|
||||
import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext'
|
||||
import { Sidebar } from 'components/interfaces/Sidebar'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCheckLatestDeploy } from 'hooks/use-check-latest-deploy'
|
||||
import { SidebarProvider } from 'ui'
|
||||
import { LayoutHeader } from './ProjectLayout/LayoutHeader'
|
||||
import MobileNavigationBar from './ProjectLayout/NavigationBar/MobileNavigationBar'
|
||||
@@ -29,6 +30,8 @@ const DefaultLayout = ({ children, headerTitle }: PropsWithChildren<DefaultLayou
|
||||
const router = useRouter()
|
||||
const showProductMenu = !!ref && router.pathname !== '/project/[ref]'
|
||||
|
||||
useCheckLatestDeploy()
|
||||
|
||||
return (
|
||||
<SidebarProvider defaultOpen={false}>
|
||||
<ProjectContextProvider projectRef={ref}>
|
||||
|
||||
@@ -11,6 +11,7 @@ export type BranchCreateVariables = {
|
||||
branchName: string
|
||||
gitBranch?: string
|
||||
region?: string
|
||||
withData?: boolean
|
||||
}
|
||||
|
||||
export async function createBranch({
|
||||
@@ -19,6 +20,7 @@ export async function createBranch({
|
||||
branchName,
|
||||
gitBranch,
|
||||
region,
|
||||
withData,
|
||||
}: BranchCreateVariables) {
|
||||
const { data, error } = await post('/v1/projects/{ref}/branches', {
|
||||
params: {
|
||||
@@ -29,6 +31,7 @@ export async function createBranch({
|
||||
branch_name: branchName,
|
||||
git_branch: gitBranch,
|
||||
region: region,
|
||||
with_data: withData,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ const WHITELIST_ERRORS = [
|
||||
'already exists in your organization.',
|
||||
]
|
||||
|
||||
type CreateProjectBody = components['schemas']['CreateProjectBody']
|
||||
type CloudProvider = CreateProjectBody['cloud_provider']
|
||||
|
||||
export type ProjectCreateVariables = {
|
||||
name: string
|
||||
organizationSlug: string
|
||||
@@ -51,8 +54,8 @@ export async function createProject({
|
||||
postgresEngine,
|
||||
releaseChannel,
|
||||
}: ProjectCreateVariables) {
|
||||
const body: components['schemas']['CreateProjectBody'] = {
|
||||
cloud_provider: cloudProvider,
|
||||
const body: CreateProjectBody = {
|
||||
cloud_provider: cloudProvider as CloudProvider,
|
||||
organization_slug: organizationSlug,
|
||||
name,
|
||||
db_pass: dbPass,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const getAuthReportAttributes = (isFreePlan: boolean) => [
|
||||
export const getAuthReportAttributes = () => [
|
||||
{
|
||||
id: 'active-users',
|
||||
label: 'Active Users',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { numberFormatter } from 'components/ui/Charts/Charts.utils'
|
||||
import { ReportAttributes } from 'components/ui/Charts/ComposedChart.utils'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { Organization } from 'types'
|
||||
import { Project } from '../projects/project-detail-query'
|
||||
import { ReportAttributes } from 'components/ui/Charts/ComposedChart.utils'
|
||||
|
||||
export const getReportAttributes = (org: Organization, project: Project): ReportAttributes[] => {
|
||||
const computeSize = project?.infra_compute_size || 'medium'
|
||||
|
||||
@@ -12,7 +12,7 @@ export const getEdgeFunctionReportAttributes = (): ReportAttributes[] => [
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'bar',
|
||||
titleTooltip: 'The total number of edge function executions by status code.',
|
||||
availableIn: ['free', 'pro', 'team'],
|
||||
availableIn: ['free', 'pro', 'team', 'enterprise'],
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'ExecutionStatusCodes',
|
||||
@@ -32,7 +32,7 @@ export const getEdgeFunctionReportAttributes = (): ReportAttributes[] => [
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
titleTooltip: 'Average execution time for edge functions.',
|
||||
availableIn: ['free', 'pro', 'team'],
|
||||
availableIn: ['free', 'pro', 'team', 'enterprise'],
|
||||
format: 'ms',
|
||||
YAxisProps: {
|
||||
width: 50,
|
||||
@@ -58,7 +58,7 @@ export const getEdgeFunctionReportAttributes = (): ReportAttributes[] => [
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'bar',
|
||||
titleTooltip: 'The total number of edge function invocations by region.',
|
||||
availableIn: ['pro', 'team'],
|
||||
availableIn: ['pro', 'team', 'enterprise'],
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'InvocationsByRegion',
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { fetchHandler } from 'data/fetchers'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export async function getDeploymentCommit(signal?: AbortSignal) {
|
||||
const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`)
|
||||
return (await response.json()) as { commitSha: string; commitTime: string }
|
||||
}
|
||||
|
||||
export type DeploymentCommitData = Awaited<ReturnType<typeof getDeploymentCommit>>
|
||||
|
||||
export const useDeploymentCommitQuery = <TData = DeploymentCommitData>({
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<DeploymentCommitData, ResponseError, TData> = {}) =>
|
||||
useQuery<DeploymentCommitData, ResponseError, TData>(
|
||||
['deployment-commit'],
|
||||
({ signal }) => getDeploymentCommit(signal),
|
||||
{
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,83 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { useDeploymentCommitQuery } from 'data/utils/deployment-commit-query'
|
||||
import { Button, StatusIcon } from 'ui'
|
||||
import { useFlag } from './ui/useFlag'
|
||||
|
||||
const DeployCheckToast = ({ id }: { id: string | number }) => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 flex-col w-full">
|
||||
<div className="flex gap-3 flex-row">
|
||||
<StatusIcon variant="default" className="mt-0.5" />
|
||||
<div className="flex w-full justify-between flex-col text-sm">
|
||||
<p className="text-foreground">A new version of this page is available</p>
|
||||
<p className="text-foreground-light">Refresh to see the latest changes.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-5 justify-end">
|
||||
<Button type="outline" onClick={() => toast.dismiss(id)}>
|
||||
Not now
|
||||
</Button>
|
||||
<Button onClick={() => router.reload()}>Refresh</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// This hook checks if the user is using old Studio pages and shows a toast to refresh the page. It's only triggered if
|
||||
// there's a new version of Studio is available, and the user has been on the old dashboard (based on commit) for more than 24 hours.
|
||||
// [Joshen] K-Dog has a suggestion here to bring down the time period here by checking commits
|
||||
export function useCheckLatestDeploy() {
|
||||
const showRefreshToast = useFlag('showRefreshToast')
|
||||
|
||||
const [currentCommitTime, setCurrentCommitTime] = useState('')
|
||||
const [isToastShown, setIsToastShown] = useState(false)
|
||||
|
||||
const { data: commit } = useDeploymentCommitQuery({
|
||||
enabled: IS_PLATFORM && showRefreshToast,
|
||||
staleTime: 10000, // 10 seconds
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// if the fetched commit is undefined is undefined
|
||||
if (!commit || commit.commitTime === 'unknown') {
|
||||
return
|
||||
}
|
||||
|
||||
// set the current commit on first load
|
||||
if (!currentCommitTime) {
|
||||
setCurrentCommitTime(commit.commitTime)
|
||||
return
|
||||
}
|
||||
|
||||
// if the current commit is the same as the fetched commit, do nothing
|
||||
if (currentCommitTime === commit.commitTime) {
|
||||
return
|
||||
}
|
||||
|
||||
// prevent showing the toast again if user has already seen and dismissed it
|
||||
if (isToastShown) {
|
||||
return
|
||||
}
|
||||
|
||||
// check if the time difference between commits is more than 24 hours
|
||||
const hourDiff = dayjs(commit.commitTime).diff(dayjs(currentCommitTime), 'hour')
|
||||
if (hourDiff < 24) {
|
||||
return
|
||||
}
|
||||
|
||||
// show the toast
|
||||
toast.custom((id) => <DeployCheckToast id={id} />, {
|
||||
duration: Infinity,
|
||||
position: 'bottom-right',
|
||||
})
|
||||
setIsToastShown(true)
|
||||
}, [commit, isToastShown, currentCommitTime])
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { PropsWithChildren, useCallback, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
import { GOTRUE_ERRORS, IS_PLATFORM } from './constants'
|
||||
|
||||
const AuthErrorToaster = ({ children }: PropsWithChildren) => {
|
||||
@@ -42,12 +43,15 @@ export { useAuth, useIsLoggedIn, useSession, useUser } from 'common'
|
||||
|
||||
export function useSignOut() {
|
||||
const queryClient = useQueryClient()
|
||||
const { clearStorage: clearAssistantStorage } = useAiAssistantStateSnapshot()
|
||||
|
||||
return useCallback(async () => {
|
||||
const result = await gotrueClient.signOut()
|
||||
clearLocalStorage()
|
||||
// Clear Assistant IndexedDB
|
||||
await clearAssistantStorage()
|
||||
await queryClient.clear()
|
||||
|
||||
return result
|
||||
}, [queryClient])
|
||||
}, [queryClient, clearAssistantStorage])
|
||||
}
|
||||
@@ -73,6 +73,11 @@ export const buildPathWithParams = (pathname: string) => {
|
||||
}
|
||||
|
||||
export const getReturnToPath = (fallback = DEFAULT_FALLBACK_PATH) => {
|
||||
// If we're in a server environment, return the fallback
|
||||
if (typeof location === 'undefined') {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
|
||||
let returnTo = searchParams.get('returnTo') ?? fallback
|
||||
|
||||
@@ -16,6 +16,7 @@ const HOSTED_SUPPORTED_API_URLS = [
|
||||
'/ai/feedback/classify',
|
||||
'/get-ip-address',
|
||||
'/get-utc-time',
|
||||
'/get-deployment-commit',
|
||||
'/check-cname',
|
||||
'/edge-functions/test',
|
||||
'/edge-functions/body',
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
async function getCommitTime(commitSha: string) {
|
||||
try {
|
||||
const response = await fetch(`https://github.com/supabase/supabase/commit/${commitSha}.json`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch commit details')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Date(data.payload.commit.committedDate).toISOString()
|
||||
} catch (error) {
|
||||
console.error('Error fetching commit time:', error)
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<{ commitSha: string; commitTime: string }>
|
||||
) {
|
||||
// Set cache control headers for 10 minutes so that we don't get banned by Github API
|
||||
res.setHeader('Cache-Control', 's-maxage=600')
|
||||
|
||||
// Get the build commit SHA from Vercel environment variable
|
||||
const commitSha = process.env.VERCEL_GIT_COMMIT_SHA || 'development'
|
||||
|
||||
// Only fetch commit time if we have a valid SHA
|
||||
const commitTime = commitSha !== 'development' ? await getCommitTime(commitSha) : 'unknown'
|
||||
|
||||
res.status(200).json({
|
||||
commitSha,
|
||||
commitTime,
|
||||
})
|
||||
}
|
||||
@@ -86,7 +86,7 @@ const ForgotPasswordMfa: NextPageWithLayout = () => {
|
||||
heading="Complete two-factor authentication"
|
||||
subheading="Enter the authentication code from your two-factor authentication app before changing your password"
|
||||
>
|
||||
<SignInMfaForm />
|
||||
<SignInMfaForm context="forgot-password" />
|
||||
</ForgotPasswordLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { useState } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowRight, RefreshCw } from 'lucide-react'
|
||||
import { useParams } from 'common'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ReportChart from 'components/interfaces/Reports/ReportChart'
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
|
||||
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import ReportChart from 'components/interfaces/Reports/ReportChart'
|
||||
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
|
||||
|
||||
import { getAuthReportAttributes } from 'data/reports/auth-charts'
|
||||
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
|
||||
import ReportFilterBar from 'components/interfaces/Reports/ReportFilterBar'
|
||||
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { SharedAPIReport } from 'components/interfaces/Reports/SharedAPIReport/SharedAPIReport'
|
||||
import { useSharedAPIReport } from 'components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants'
|
||||
import ReportFilterBar from 'components/interfaces/Reports/ReportFilterBar'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import { getAuthReportAttributes } from 'data/reports/auth-charts'
|
||||
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const AuthReport: NextPageWithLayout = () => {
|
||||
return (
|
||||
@@ -47,8 +47,6 @@ const AuthUsage = () => {
|
||||
updateDateRange,
|
||||
datePickerValue,
|
||||
datePickerHelpers,
|
||||
isOrgPlanLoading,
|
||||
orgPlan,
|
||||
showUpgradePrompt,
|
||||
setShowUpgradePrompt,
|
||||
handleDatePickerChange,
|
||||
@@ -73,8 +71,7 @@ const AuthUsage = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
|
||||
const isFreePlan = !isOrgPlanLoading && orgPlan?.id === 'free'
|
||||
const AUTH_REPORT_ATTRIBUTES = getAuthReportAttributes(isFreePlan)
|
||||
const AUTH_REPORT_ATTRIBUTES = getAuthReportAttributes()
|
||||
|
||||
const onRefreshReport = async () => {
|
||||
if (!selectedDateRange) return
|
||||
@@ -142,7 +139,6 @@ const AuthUsage = () => {
|
||||
startDate={selectedDateRange?.period_start?.date}
|
||||
endDate={selectedDateRange?.period_end?.date}
|
||||
updateDateRange={updateDateRange}
|
||||
orgPlanId={orgPlan?.id}
|
||||
isLoading={isRefreshing}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowRight, ExternalLink, RefreshCw } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { AlertDescription_Shadcn_, Alert_Shadcn_, Button } from 'ui'
|
||||
import { useParams } from 'common'
|
||||
|
||||
import ReportChart from 'components/interfaces/Reports/ReportChart'
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
|
||||
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
import DiskSizeConfigurationModal from 'components/interfaces/Settings/Database/DiskSizeConfigurationModal'
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import ChartHandler from 'components/ui/Charts/ChartHandler'
|
||||
import ComposedChartHandler from 'components/ui/Charts/ComposedChartHandler'
|
||||
import GrafanaPromoBanner from 'components/ui/GrafanaPromoBanner'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import ComposedChartHandler from 'components/ui/Charts/ComposedChartHandler'
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
|
||||
import GrafanaPromoBanner from 'components/ui/GrafanaPromoBanner'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import ReportChart from 'components/interfaces/Reports/ReportChart'
|
||||
|
||||
import { analyticsKeys } from 'data/analytics/keys'
|
||||
import { getReportAttributes, getReportAttributesV2 } from 'data/reports/database-charts'
|
||||
import { useDatabaseSizeQuery } from 'data/database/database-size-query'
|
||||
import { useDatabaseReport } from 'data/reports/database-report-query'
|
||||
import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mutation'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
|
||||
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
|
||||
import { analyticsKeys } from 'data/analytics/keys'
|
||||
import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mutation'
|
||||
import { useDatabaseSizeQuery } from 'data/database/database-size-query'
|
||||
import { getReportAttributes, getReportAttributesV2 } from 'data/reports/database-charts'
|
||||
import { useDatabaseReport } from 'data/reports/database-report-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const DatabaseReport: NextPageWithLayout = () => {
|
||||
return (
|
||||
@@ -288,8 +288,6 @@ const DatabaseUsage = () => {
|
||||
startDate={selectedDateRange?.period_start?.date}
|
||||
endDate={selectedDateRange?.period_end?.date}
|
||||
updateDateRange={updateDateRange}
|
||||
orgPlanId={orgPlan?.id}
|
||||
availableIn={chart.availableIn}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowRight, ChevronDown, RefreshCw } from 'lucide-react'
|
||||
import { useParams } from 'common'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Label } from '@ui/components/shadcn/ui/label'
|
||||
import ReportChart from 'components/interfaces/Reports/ReportChart'
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
|
||||
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
|
||||
import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import ReportsLayout from 'components/layouts/ReportsLayout/ReportsLayout'
|
||||
import ReportChart from 'components/interfaces/Reports/ReportChart'
|
||||
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import {
|
||||
LogsDatePicker,
|
||||
DatePickerValue,
|
||||
} from 'components/interfaces/Settings/Logs/Logs.DatePickers'
|
||||
import { Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui'
|
||||
import { Label } from '@ui/components/shadcn/ui/label'
|
||||
|
||||
import { getEdgeFunctionReportAttributes } from 'data/reports/edgefn-charts'
|
||||
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
|
||||
import { getEdgeFunctionReportAttributes } from 'data/reports/edgefn-charts'
|
||||
|
||||
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
|
||||
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
|
||||
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
|
||||
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
@@ -226,7 +223,6 @@ const EdgeFunctionsUsage = () => {
|
||||
endDate={selectedDateRange?.period_end?.date}
|
||||
updateDateRange={updateDateRange}
|
||||
functionIds={functionIds}
|
||||
orgPlanId={orgPlan?.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -95,6 +95,15 @@ async function saveAiState(state: StoredAiAssistantState): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function clearStorage(): Promise<void> {
|
||||
try {
|
||||
const db = await openAiDb()
|
||||
await db.clear(STORE_NAME)
|
||||
} catch (error) {
|
||||
console.error('Failed to clear AI state from IndexedDB:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to load state from IndexedDB
|
||||
async function loadFromIndexedDB(projectRef: string): Promise<StoredAiAssistantState | null> {
|
||||
try {
|
||||
@@ -387,6 +396,10 @@ export const createAiAssistantState = (): AiAssistantState => {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
clearStorage: async () => {
|
||||
await clearStorage()
|
||||
},
|
||||
})
|
||||
|
||||
return state
|
||||
@@ -413,6 +426,7 @@ export type AiAssistantState = AiAssistantData & {
|
||||
clearSqlSnippets: () => void
|
||||
getCachedSQLResults: (args: { messageId: string; snippetId?: string }) => any[] | undefined
|
||||
loadPersistedState: (persistedState: StoredAiAssistantState) => void
|
||||
clearStorage: () => Promise<void>
|
||||
}
|
||||
|
||||
export const AiAssistantStateContext = createContext<AiAssistantState>(createAiAssistantState())
|
||||
|
||||
@@ -49,7 +49,7 @@ There will be a winner and a runner-up prize for each category. Every team membe
|
||||
|
||||
## Submission
|
||||
|
||||
You should submit your project from the submission form before 11:59 pm Sunday midnight PT, July 27th, 2025. The submission form will be put up here on this article before the deadline. Come back in about a week to find it!
|
||||
You should submit your project from the [submission form](https://docs.google.com/forms/d/e/1FAIpQLSdWEAPmLpE3SjfOlR388eY3i901VEFTqsEr9eFJyZVGG47pQQ/viewform?usp=header) before 11:59 pm Sunday midnight PT, July 27th, 2025.
|
||||
|
||||
## Judges
|
||||
|
||||
|
||||
@@ -795,7 +795,7 @@ Supabase's Email Login feature enables secure email-based authentication for you
|
||||
- Secure authentication: Implement industry-standard security practices.
|
||||
- Customizable workflows: Tailor the signup and login processes to your needs.
|
||||
- Seamless integration: Works with Supabase's other auth providers and features.
|
||||
- Local development support: Test email flows using built-in tools like Inbucket.
|
||||
- Local development support: Test email flows using built-in tools like Mailpit.
|
||||
|
||||
## Email login is valuable for:
|
||||
- SaaS applications requiring user accounts
|
||||
|
||||
@@ -10,6 +10,7 @@ trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
|
||||
@@ -5,94 +5,86 @@
|
||||
"projects": {
|
||||
"angular-user-management": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"standalone": false
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist/angular-user-management",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
"styles": ["src/styles.css"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular-user-management:build:production"
|
||||
"buildTarget": "angular-user-management:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "angular-user-management:build:development"
|
||||
"buildTarget": "angular-user-management:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular-user-management:build"
|
||||
}
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
"styles": ["src/styles.css"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/angular-user-management'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -8,32 +8,40 @@
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"prettier": {
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^14.2.0",
|
||||
"@angular/common": "^14.2.0",
|
||||
"@angular/compiler": "^14.2.0",
|
||||
"@angular/core": "^14.2.0",
|
||||
"@angular/forms": "^14.2.0",
|
||||
"@angular/platform-browser": "^14.2.0",
|
||||
"@angular/platform-browser-dynamic": "^14.2.0",
|
||||
"@angular/router": "^14.2.0",
|
||||
"@supabase/supabase-js": "^2.0.4",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.11.4"
|
||||
"@angular/common": "^20.1.2",
|
||||
"@angular/compiler": "^20.1.2",
|
||||
"@angular/core": "^20.1.2",
|
||||
"@angular/forms": "^20.1.2",
|
||||
"@angular/platform-browser": "^20.1.2",
|
||||
"@angular/router": "^20.1.2",
|
||||
"@supabase/supabase-js": "^2.52.0",
|
||||
"rxjs": "~7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "~0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^14.2.5",
|
||||
"@angular/cli": "~14.2.5",
|
||||
"@angular/compiler-cli": "^14.2.0",
|
||||
"@types/jasmine": "~4.0.0",
|
||||
"jasmine-core": "~4.3.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"@angular/build": "^20.1.1",
|
||||
"@angular/cli": "^20.1.1",
|
||||
"@angular/compiler-cli": "^20.1.2",
|
||||
"@types/jasmine": "~5.1.8",
|
||||
"jasmine-core": "~5.9.0",
|
||||
"karma": "~6.4.4",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.1",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"typescript": "~4.7.2"
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.8.3"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
-40
@@ -1,40 +0,0 @@
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<app-avatar
|
||||
[avatarUrl]="this.avatarUrl"
|
||||
(upload)="updateAvatar($event)">
|
||||
</app-avatar>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" [value]="session.user.email" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input
|
||||
formControlName="username"
|
||||
id="username"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input
|
||||
formControlName="website"
|
||||
id="website"
|
||||
type="url"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="button primary block"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ loading ? 'Loading ...' : 'Update' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="button block" (click)="signOut()">Sign Out</button>
|
||||
</div>
|
||||
</form>
|
||||
File renamed without changes.
@@ -0,0 +1,30 @@
|
||||
<form
|
||||
[formGroup]="updateProfileForm"
|
||||
(ngSubmit)="updateProfile()"
|
||||
class="form-widget"
|
||||
>
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)">
|
||||
</app-avatar>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" [value]="session.user.email" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input formControlName="username" id="username" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input formControlName="website" id="website" type="url" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="button primary block" [disabled]="loading">
|
||||
{{ loading ? "Loading ..." : "Update" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="button block" (click)="signOut()">Sign Out</button>
|
||||
</div>
|
||||
</form>
|
||||
+4
-5
@@ -1,16 +1,15 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccountComponent } from './account.component';
|
||||
import { AccountComponent } from './account';
|
||||
|
||||
describe('AccountComponent', () => {
|
||||
describe('Account', () => {
|
||||
let component: AccountComponent;
|
||||
let fixture: ComponentFixture<AccountComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AccountComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
declarations: [AccountComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AccountComponent);
|
||||
component = fixture.componentInstance;
|
||||
+27
-22
@@ -1,30 +1,42 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { AuthSession } from '@supabase/supabase-js';
|
||||
import { Profile, SupabaseService } from '../supabase.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.component.html',
|
||||
styleUrls: ['./account.component.css'],
|
||||
templateUrl: './account.html',
|
||||
styleUrls: ['./account.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
loading = false;
|
||||
profile!: Profile;
|
||||
updateProfileForm!: FormGroup;
|
||||
|
||||
get avatarUrl() {
|
||||
return this.updateProfileForm.value.avatar_url as string;
|
||||
}
|
||||
async updateAvatar(event: string): Promise<void> {
|
||||
this.updateProfileForm.patchValue({
|
||||
avatar_url: event,
|
||||
});
|
||||
await this.updateProfile();
|
||||
}
|
||||
|
||||
@Input()
|
||||
session!: AuthSession;
|
||||
|
||||
updateProfileForm = this.formBuilder.group({
|
||||
username: '',
|
||||
website: '',
|
||||
avatar_url: '',
|
||||
});
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private formBuilder: FormBuilder
|
||||
) {}
|
||||
) {
|
||||
this.updateProfileForm = this.formBuilder.group({
|
||||
username: '',
|
||||
website: '',
|
||||
avatar_url: '',
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.getProfile();
|
||||
@@ -37,15 +49,15 @@ export class AccountComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
get avatarUrl() {
|
||||
return this.updateProfileForm.value.avatar_url as string;
|
||||
}
|
||||
|
||||
async getProfile() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const { user } = this.session;
|
||||
let { data: profile, error, status } = await this.supabase.profile(user);
|
||||
const {
|
||||
data: profile,
|
||||
error,
|
||||
status,
|
||||
} = await this.supabase.profile(user);
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error;
|
||||
@@ -63,13 +75,6 @@ export class AccountComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async updateAvatar(event: string): Promise<void> {
|
||||
this.updateProfileForm.patchValue({
|
||||
avatar_url: event,
|
||||
});
|
||||
await this.updateProfile();
|
||||
}
|
||||
|
||||
async updateProfile(): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,11 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { AuthComponent } from './auth/auth.component';
|
||||
import { AccountComponent } from './account/account.component';
|
||||
import { AppComponent } from './app';
|
||||
import { AuthComponent } from './auth/auth';
|
||||
import { AccountComponent } from './account/account';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { AvatarComponent } from './avatar/avatar.component';
|
||||
import { AvatarComponent } from './avatar/avatar';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -17,5 +17,6 @@ import { AvatarComponent } from './avatar/avatar.component';
|
||||
imports: [BrowserModule, ReactiveFormsModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
exports: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
+8
-12
@@ -1,12 +1,12 @@
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppComponent } from './app';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
declarations: [AppComponent],
|
||||
providers: [provideZonelessChangeDetection()],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -16,16 +16,12 @@ describe('AppComponent', () => {
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'angular-user-management'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('angular-user-management');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('.content span')?.textContent).toContain('angular-user-management app is running!');
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain(
|
||||
'Hello, supabase-angular'
|
||||
);
|
||||
});
|
||||
});
|
||||
+7
-6
@@ -3,17 +3,18 @@ import { SupabaseService } from './supabase.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css'],
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ['./app.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
title = 'angular-user-management';
|
||||
|
||||
session = this.supabase.session;
|
||||
|
||||
constructor(private readonly supabase: SupabaseService) {}
|
||||
|
||||
title = 'angular-user-management';
|
||||
session: any;
|
||||
|
||||
ngOnInit() {
|
||||
this.session = this.supabase.session;
|
||||
this.supabase.authChanges((_, session) => (this.session = session));
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+2
-6
@@ -14,12 +14,8 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="button block"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ loading ? 'Loading' : 'Send magic link' }}
|
||||
<button type="submit" class="button block" [disabled]="loading">
|
||||
{{ loading ? "Loading" : "Send magic link" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
+4
-5
@@ -1,16 +1,15 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthComponent } from './auth.component';
|
||||
import { AuthComponent } from './auth';
|
||||
|
||||
describe('AuthComponent', () => {
|
||||
describe('Auth', () => {
|
||||
let component: AuthComponent;
|
||||
let fixture: ComponentFixture<AuthComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AuthComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
declarations: [AuthComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AuthComponent);
|
||||
component = fixture.componentInstance;
|
||||
+13
-12
@@ -1,25 +1,26 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { SupabaseService } from '../supabase.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth',
|
||||
templateUrl: './auth.component.html',
|
||||
styleUrls: ['./auth.component.css'],
|
||||
templateUrl: './auth.html',
|
||||
styleUrls: ['./auth.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AuthComponent implements OnInit {
|
||||
loading = false;
|
||||
|
||||
signInForm = this.formBuilder.group({
|
||||
email: '',
|
||||
});
|
||||
|
||||
export class AuthComponent {
|
||||
signInForm!: FormGroup;
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
loading = false;
|
||||
ngOnInit() {
|
||||
this.signInForm = this.formBuilder.group({
|
||||
email: '',
|
||||
});
|
||||
}
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
try {
|
||||
File renamed without changes.
+2
-2
@@ -14,10 +14,10 @@
|
||||
></div>
|
||||
<div style="width: 150px">
|
||||
<label class="button primary block" for="single">
|
||||
{{ uploading ? 'Uploading ...' : 'Upload' }}
|
||||
{{ uploading ? "Uploading ..." : "Upload" }}
|
||||
</label>
|
||||
<input
|
||||
style="visibility: hidden;position: absolute"
|
||||
style="visibility: hidden; position: absolute"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
+4
-5
@@ -1,16 +1,15 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AvatarComponent } from './avatar.component';
|
||||
import { AvatarComponent } from './avatar';
|
||||
|
||||
describe('AvatarComponent', () => {
|
||||
describe('Avatar', () => {
|
||||
let component: AvatarComponent;
|
||||
let fixture: ComponentFixture<AvatarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AvatarComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
declarations: [AvatarComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AvatarComponent);
|
||||
component = fixture.componentInstance;
|
||||
+5
-6
@@ -1,13 +1,14 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser';
|
||||
import { SupabaseService } from '../supabase.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-avatar',
|
||||
templateUrl: './avatar.component.html',
|
||||
styleUrls: ['./avatar.component.css'],
|
||||
templateUrl: './avatar.html',
|
||||
styleUrls: ['./avatar.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AvatarComponent implements OnInit {
|
||||
export class AvatarComponent {
|
||||
_avatarUrl: SafeResourceUrl | undefined;
|
||||
uploading = false;
|
||||
|
||||
@@ -25,8 +26,6 @@ export class AvatarComponent implements OnInit {
|
||||
private readonly dom: DomSanitizer
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
async downloadImage(path: string) {
|
||||
try {
|
||||
const { data } = await this.supabase.downLoadImage(path);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SupabaseService } from './supabase.service';
|
||||
|
||||
describe('SupabaseService', () => {
|
||||
let service: SupabaseService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(SupabaseService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
SupabaseClient,
|
||||
User,
|
||||
} from '@supabase/supabase-js';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Database } from 'src/schema';
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
export interface Profile {
|
||||
id?: string;
|
||||
@@ -21,11 +20,11 @@ export interface Profile {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SupabaseService {
|
||||
private supabase: SupabaseClient<Database>;
|
||||
private supabase: SupabaseClient;
|
||||
_session: AuthSession | null = null;
|
||||
|
||||
constructor() {
|
||||
this.supabase = createClient<Database>(
|
||||
this.supabase = createClient(
|
||||
environment.supabaseUrl,
|
||||
environment.supabaseKey
|
||||
);
|
||||
|
||||
Whitespace-only changes.
Binary file not shown.
|
Before Width: | Height: | Size: 948 B |
@@ -1,13 +1,13 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AngularUserManagement</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>SupabaseAngular</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +1,8 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { platformBrowser } from '@angular/platform-browser';
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
platformBrowser()
|
||||
.bootstrapModule(AppModule, {
|
||||
ngZoneEventCoalescing: true,
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
||||
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
@@ -1,40 +0,0 @@
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
|
||||
|
||||
export interface Database {
|
||||
public: {
|
||||
Tables: {
|
||||
profiles: {
|
||||
Row: {
|
||||
id: string
|
||||
updated_at: string | null
|
||||
username: string | null
|
||||
avatar_url: string | null
|
||||
website: string | null
|
||||
}
|
||||
Insert: {
|
||||
id: string
|
||||
updated_at?: string | null
|
||||
username?: string | null
|
||||
avatar_url?: string | null
|
||||
website?: string | null
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
updated_at?: string | null
|
||||
username?: string | null
|
||||
avatar_url?: string | null
|
||||
website?: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,57 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
html,
|
||||
body {
|
||||
--custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--custom-bg-color: #101010;
|
||||
--custom-panel-color: #222;
|
||||
--custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8);
|
||||
--custom-color: #fff;
|
||||
--custom-color-brand: #24b47e;
|
||||
--custom-color-secondary: #666;
|
||||
--custom-border: 1px solid #333;
|
||||
--custom-border-radius: 5px;
|
||||
--custom-spacing: 5px;
|
||||
--custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--custom-bg-color: #101010;
|
||||
--custom-panel-color: #222;
|
||||
--custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8);
|
||||
--custom-color: #fff;
|
||||
--custom-color-brand: #24b47e;
|
||||
--custom-color-secondary: #666;
|
||||
--custom-border: 1px solid #333;
|
||||
--custom-border-radius: 5px;
|
||||
--custom-spacing: 5px;
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: var(--custom-font-family);
|
||||
background-color: var(--custom-bg-color);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: var(--custom-font-family);
|
||||
background-color: var(--custom-bg-color);
|
||||
}
|
||||
|
||||
* {
|
||||
color: var(--custom-color);
|
||||
font-family: var(--custom-font-family);
|
||||
box-sizing: border-box;
|
||||
color: var(--custom-color);
|
||||
font-family: var(--custom-font-family);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#__next {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
|
||||
.container {
|
||||
width: 90%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 90%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row [class^="col"] {
|
||||
float: left;
|
||||
margin: 0.5rem 2%;
|
||||
min-height: 0.125rem;
|
||||
float: left;
|
||||
margin: 0.5rem 2%;
|
||||
min-height: 0.125rem;
|
||||
}
|
||||
|
||||
.col-1,
|
||||
.col-2,
|
||||
.col-3,
|
||||
@@ -60,313 +64,363 @@ body,
|
||||
.col-10,
|
||||
.col-11,
|
||||
.col-12 {
|
||||
width: 96%;
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
.col-1-sm {
|
||||
width: 4.33%;
|
||||
width: 4.33%;
|
||||
}
|
||||
|
||||
.col-2-sm {
|
||||
width: 12.66%;
|
||||
width: 12.66%;
|
||||
}
|
||||
|
||||
.col-3-sm {
|
||||
width: 21%;
|
||||
width: 21%;
|
||||
}
|
||||
|
||||
.col-4-sm {
|
||||
width: 29.33%;
|
||||
width: 29.33%;
|
||||
}
|
||||
|
||||
.col-5-sm {
|
||||
width: 37.66%;
|
||||
width: 37.66%;
|
||||
}
|
||||
|
||||
.col-6-sm {
|
||||
width: 46%;
|
||||
width: 46%;
|
||||
}
|
||||
|
||||
.col-7-sm {
|
||||
width: 54.33%;
|
||||
width: 54.33%;
|
||||
}
|
||||
|
||||
.col-8-sm {
|
||||
width: 62.66%;
|
||||
width: 62.66%;
|
||||
}
|
||||
|
||||
.col-9-sm {
|
||||
width: 71%;
|
||||
width: 71%;
|
||||
}
|
||||
|
||||
.col-10-sm {
|
||||
width: 79.33%;
|
||||
width: 79.33%;
|
||||
}
|
||||
|
||||
.col-11-sm {
|
||||
width: 87.66%;
|
||||
width: 87.66%;
|
||||
}
|
||||
|
||||
.col-12-sm {
|
||||
width: 96%;
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
.row::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.hidden-sm {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 33.75em) {
|
||||
/* 540px */
|
||||
.container {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
/* 540px */
|
||||
.container {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 45em) {
|
||||
/* 720px */
|
||||
.col-1 {
|
||||
width: 4.33%;
|
||||
}
|
||||
.col-2 {
|
||||
width: 12.66%;
|
||||
}
|
||||
.col-3 {
|
||||
width: 21%;
|
||||
}
|
||||
.col-4 {
|
||||
width: 29.33%;
|
||||
}
|
||||
.col-5 {
|
||||
width: 37.66%;
|
||||
}
|
||||
.col-6 {
|
||||
width: 46%;
|
||||
}
|
||||
.col-7 {
|
||||
width: 54.33%;
|
||||
}
|
||||
.col-8 {
|
||||
width: 62.66%;
|
||||
}
|
||||
.col-9 {
|
||||
width: 71%;
|
||||
}
|
||||
.col-10 {
|
||||
width: 79.33%;
|
||||
}
|
||||
.col-11 {
|
||||
width: 87.66%;
|
||||
}
|
||||
.col-12 {
|
||||
width: 96%;
|
||||
}
|
||||
.hidden-sm {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 720px */
|
||||
.col-1 {
|
||||
width: 4.33%;
|
||||
}
|
||||
|
||||
.col-2 {
|
||||
width: 12.66%;
|
||||
}
|
||||
|
||||
.col-3 {
|
||||
width: 21%;
|
||||
}
|
||||
|
||||
.col-4 {
|
||||
width: 29.33%;
|
||||
}
|
||||
|
||||
.col-5 {
|
||||
width: 37.66%;
|
||||
}
|
||||
|
||||
.col-6 {
|
||||
width: 46%;
|
||||
}
|
||||
|
||||
.col-7 {
|
||||
width: 54.33%;
|
||||
}
|
||||
|
||||
.col-8 {
|
||||
width: 62.66%;
|
||||
}
|
||||
|
||||
.col-9 {
|
||||
width: 71%;
|
||||
}
|
||||
|
||||
.col-10 {
|
||||
width: 79.33%;
|
||||
}
|
||||
|
||||
.col-11 {
|
||||
width: 87.66%;
|
||||
}
|
||||
|
||||
.col-12 {
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
.hidden-sm {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 60em) {
|
||||
/* 960px */
|
||||
.container {
|
||||
width: 75%;
|
||||
max-width: 60rem;
|
||||
}
|
||||
|
||||
/* 960px */
|
||||
.container {
|
||||
width: 75%;
|
||||
max-width: 60rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
color: var(--custom-color-secondary);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
color: var(--custom-color-secondary);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
border: var(--custom-border);
|
||||
padding: 8px;
|
||||
font-size: 0.9rem;
|
||||
background-color: var(--custom-bg-color);
|
||||
color: var(--custom-color);
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
border: var(--custom-border);
|
||||
padding: 8px;
|
||||
font-size: 0.9rem;
|
||||
background-color: var(--custom-bg-color);
|
||||
color: var(--custom-color);
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
color: var(--custom-color-secondary);
|
||||
color: var(--custom-color-secondary);
|
||||
}
|
||||
|
||||
/* Utils */
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex.column {
|
||||
flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex.row {
|
||||
flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex.flex-1 {
|
||||
flex: 1 1 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 300;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.font-light {
|
||||
font-weight: 300;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.opacity-half {
|
||||
opacity: 50%;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
|
||||
button,
|
||||
.button {
|
||||
color: var(--custom-color);
|
||||
border: var(--custom-border);
|
||||
background-color: var(--custom-bg-color);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border-radius: var(--custom-border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--custom-color);
|
||||
border: var(--custom-border);
|
||||
background-color: var(--custom-bg-color);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border-radius: var(--custom-border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
button.primary,
|
||||
.button.primary {
|
||||
background-color: var(--custom-color-brand);
|
||||
border: 1px solid var(--custom-color-brand);
|
||||
background-color: var(--custom-color-brand);
|
||||
border: 1px solid var(--custom-color-brand);
|
||||
}
|
||||
|
||||
/* Widgets */
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: var(--custom-border);
|
||||
border-radius: var(--custom-border-radius);
|
||||
padding: var(--custom-spacing);
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: var(--custom-border);
|
||||
border-radius: var(--custom-border-radius);
|
||||
padding: var(--custom-spacing);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: var(--custom-border-radius);
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
border-radius: var(--custom-border-radius);
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.avatar.image {
|
||||
object-fit: cover;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar.no-image {
|
||||
background-color: #333;
|
||||
border: 1px solid rgb(200, 200, 200);
|
||||
border-radius: 5px;
|
||||
background-color: #333;
|
||||
border: 1px solid rgb(200, 200, 200);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
border-top: var(--custom-border);
|
||||
background-color: var(--custom-bg-color);
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
border-top: var(--custom-border);
|
||||
background-color: var(--custom-bg-color);
|
||||
}
|
||||
|
||||
.footer div {
|
||||
padding: var(--custom-spacing);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: var(--custom-spacing);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.footer div > img {
|
||||
height: 20px;
|
||||
margin-left: 10px;
|
||||
|
||||
.footer div>img {
|
||||
height: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.footer > div:first-child {
|
||||
display: none;
|
||||
|
||||
.footer>div:first-child {
|
||||
display: none;
|
||||
}
|
||||
.footer > div:nth-child(2) {
|
||||
justify-content: left;
|
||||
|
||||
.footer>div:nth-child(2) {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 60em) {
|
||||
/* 960px */
|
||||
.footer > div:first-child {
|
||||
display: flex;
|
||||
}
|
||||
.footer > div:nth-child(2) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 960px */
|
||||
.footer>div:first-child {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.footer>div:nth-child(2) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mainHeader {
|
||||
width: 100%;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.avatarPlaceholder {
|
||||
border: var(--custom-border);
|
||||
border-radius: var(--custom-border-radius);
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: var(--custom-border);
|
||||
border-radius: var(--custom-border-radius);
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-widget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-widget > .button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background-color: #444444;
|
||||
text-transform: none !important;
|
||||
transition: all 0.2s ease;
|
||||
.form-widget>.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background-color: #444444;
|
||||
text-transform: none !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-widget .button:hover {
|
||||
background-color: #2a2a2a;
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.form-widget .button > .loader {
|
||||
width: 17px;
|
||||
animation: spin 1s linear infinite;
|
||||
filter: invert(1);
|
||||
}
|
||||
.form-widget .button>.loader {
|
||||
width: 17px;
|
||||
animation: spin 1s linear infinite;
|
||||
filter: invert(1);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: {
|
||||
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||
<T>(id: string): T;
|
||||
keys(): string[];
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting(),
|
||||
);
|
||||
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().forEach(context);
|
||||
@@ -1,15 +1,15 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,32 +1,34 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2020",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
]
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
"types": ["jasmine"]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||
}
|
||||
Vendored
+9
@@ -2450,6 +2450,11 @@ export interface components {
|
||||
grant_type?: 'authorization_code' | 'refresh_token'
|
||||
redirect_uri?: string
|
||||
refresh_token?: string
|
||||
/**
|
||||
* @description Resource indicator for MCP (Model Context Protocol) clients
|
||||
* @enum {string}
|
||||
*/
|
||||
resource?: 'http://localhost:8080/mcp'
|
||||
}
|
||||
OAuthTokenResponse: {
|
||||
access_token: string
|
||||
@@ -3697,7 +3702,11 @@ export interface operations {
|
||||
client_id: string
|
||||
code_challenge?: string
|
||||
code_challenge_method?: 'plain' | 'sha256' | 'S256'
|
||||
/** @description Organization slug */
|
||||
organization_slug?: string
|
||||
redirect_uri: string
|
||||
/** @description Resource indicator for MCP (Model Context Protocol) clients */
|
||||
resource?: 'http://localhost:8080/mcp'
|
||||
response_mode?: string
|
||||
response_type: 'code' | 'token' | 'id_token token'
|
||||
scope?: string
|
||||
|
||||
+29
-11
@@ -5372,11 +5372,6 @@ export interface components {
|
||||
file_url: string
|
||||
fileUrl: string
|
||||
}
|
||||
GetAvailableRegionsBody: {
|
||||
/** @enum {string} */
|
||||
cloud_provider: 'AWS' | 'FLY' | 'AWS_K8S'
|
||||
organization_slug: string
|
||||
}
|
||||
GetContentCountResponse: {
|
||||
count: number
|
||||
}
|
||||
@@ -11938,6 +11933,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['InvitationResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to get organization invitations */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -11969,6 +11970,12 @@ export interface operations {
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to create organization invitation */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -18461,6 +18468,12 @@ export interface operations {
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to get project's status */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -18534,16 +18547,15 @@ export interface operations {
|
||||
}
|
||||
ProjectsController_getRegions: {
|
||||
parameters: {
|
||||
query?: never
|
||||
query: {
|
||||
cloud_provider: 'AWS' | 'FLY' | 'AWS_K8S'
|
||||
organization_slug: string
|
||||
}
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['GetAvailableRegionsBody']
|
||||
}
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
@@ -18574,6 +18586,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['GetProjectByFlyExtensionIdResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
ReplicationDestinationsController_getDestinations: {
|
||||
|
||||
@@ -124,6 +124,7 @@ may_uppercase = [
|
||||
"Llamafile",
|
||||
"Logs Explorer",
|
||||
"Magic Link",
|
||||
"Mailpit",
|
||||
"Management API",
|
||||
"Mixpeek",
|
||||
"Mixpeek Embed",
|
||||
|
||||
@@ -204,6 +204,7 @@ allow_list = [
|
||||
"Logflare",
|
||||
"Lua",
|
||||
"Mailgun",
|
||||
"Mailpit",
|
||||
"Mailtrap",
|
||||
"Mansueli",
|
||||
"Metabase",
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@
|
||||
"AI_PRO_MODEL",
|
||||
"AI_NORMAL_MODEL"
|
||||
],
|
||||
"passThroughEnv": ["CURRENT_CLI_VERSION"],
|
||||
"passThroughEnv": ["CURRENT_CLI_VERSION", "VERCEL_GIT_COMMIT_SHA"],
|
||||
"outputs": [".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"www#build": {
|
||||
|
||||
Reference in new issue
Block a user